Compare commits

...

14 Commits

Author SHA1 Message Date
johnlanni
b5eadcdbee release v2.1.3 2025-05-09 15:30:22 +08:00
EricaLiu
8ca8fd27ab fix param type error (#2204) 2025-05-09 14:55:10 +08:00
Kent Dong
ab014cf912 feat: Add SSE direct proxy support to mcp-session filter (#2157) 2025-05-09 14:28:42 +08:00
EricaLiu
3f67b05fab fix : fix vs rewrite when mcp protocol is http (#2203) 2025-05-09 14:03:31 +08:00
HaoJie Liu
cd271c1f87 fix(ai-statistics): adjust requestBodyBufferLimit (#2192)
Co-authored-by: Kent Dong <ch3cho@qq.com>
2025-05-08 16:18:50 +08:00
johnlanni
755de5ae67 add original path info in mcp-server 2025-05-07 21:17:18 +08:00
johnlanni
40402e7dbd refactor route call in mcp-server 2025-05-07 20:36:41 +08:00
johnlanni
0a2fb35ae2 fix gemini provider in ai-proxy 2025-05-07 16:54:40 +08:00
澄潭
b16954d8c1 Update README.md 2025-05-07 15:27:28 +08:00
Kent Dong
29370b18d7 feat: Support /v1/models API in ai-proxy (#2164) 2025-05-06 15:53:13 +08:00
EricaLiu
c9733d405c fix : Add nacos username and password login option (#2170) 2025-05-06 15:18:45 +08:00
johnlanni
ec6004dd27 update golang filter dependency 2025-04-30 23:33:04 +08:00
Jingze
ea9a6de8c3 fix: update golang filter README (#2147) 2025-04-29 22:08:10 +08:00
github-actions[bot]
5e40a700ae Update helm translated README.zh.md (#2152) 2025-04-29 21:04:23 +08:00
45 changed files with 795 additions and 271 deletions

View File

@@ -1 +1 @@
v2.1.2
v2.1.3

View File

@@ -1,5 +1,5 @@
apiVersion: v2
appVersion: 2.1.2
appVersion: 2.1.3
description: Helm chart for deploying higress gateways
icon: https://higress.io/img/higress_logo_small.png
home: http://higress.io/
@@ -15,4 +15,4 @@ dependencies:
repository: "file://../redis"
version: 0.0.1
type: application
version: 2.1.2
version: 2.1.3

View File

@@ -1,9 +1,9 @@
dependencies:
- name: higress-core
repository: file://../core
version: 2.1.2
version: 2.1.3
- name: higress-console
repository: https://higress.io/helm-charts/
version: 2.1.2
digest: sha256:7612de239141ca0d27400f7d5b9a786acd98826f511e2e3ed65ccd9d2c9f1700
generated: "2025-04-29T20:52:39.996652+08:00"
version: 2.1.3
digest: sha256:c7307d5398c3c1178758c5372bd1aa4cb8dee7beeab3832d3e9ce0a04d1adc23
generated: "2025-05-09T15:29:50.616179+08:00"

View File

@@ -1,5 +1,5 @@
apiVersion: v2
appVersion: 2.1.2
appVersion: 2.1.3
description: Helm chart for deploying Higress gateways
icon: https://higress.io/img/higress_logo_small.png
home: http://higress.io/
@@ -12,9 +12,9 @@ sources:
dependencies:
- name: higress-core
repository: "file://../core"
version: 2.1.2
version: 2.1.3
- name: higress-console
repository: "https://higress.io/helm-charts/"
version: 2.1.2
version: 2.1.3
type: application
version: 2.1.2
version: 2.1.3

View File

@@ -1,8 +1,8 @@
## Higress for Kubernetes
## Higress 适用于 Kubernetes
Higress 是基于阿里巴巴内部网关实践的云原生 API 网关。
通过基于 Istio 和 EnvoyHigress 实现了流量网关、微服务网关和安全网关的三重网关架构的集成,从而大大降低了部署、运维的成本。
通过 Istio 和 Envoy 的支持Higress 实现了流量网关、微服务网关和安全网关三种架构的融合,从而极大地减少了部署、运维的成本。
## 设置仓库信息
@@ -13,7 +13,7 @@ helm repo update
## 安装
使用名为 `higress`版本来安装 chart
使用 Helm 安装名为 `higress`组件
```console
helm install higress -n higress-system higress.io/higress --create-namespace --render-subchart-notes
@@ -21,60 +21,130 @@ helm install higress -n higress-system higress.io/higress --create-namespace --r
## 卸载
卸载删除 higress 部署
删除名称为 higress 的安装
```console
helm delete higress -n higress-system
```
该命令删除与 chart 相关的所有 Kubernetes 组件并删除发行版。
该命令删除与组件关联的所有 Kubernetes 组件并卸载该发行版。
## 参数
## 配置值
## Values
| 键 | 类型 | 默认值 | 描述 |
|------|------|---------|-------------|
| clusterName | string | `""` | |
| controller.affinity | object | `{}` | |
| controller.automaticHttps.email | string | `""` | |
| controller.automaticHttps.enabled | bool | `true` | |
| controller.autoscaling.enabled | bool | `false` | |
| controller.autoscaling.maxReplicas | int | `5` | |
| controller.autoscaling.minReplicas | int | `1` | |
| controller.autoscaling.targetCPUUtilizationPercentage | int | `80` | |
| controller.env | object | `{}` | |
| controller.hub | string | `"higress-registry.cn-hangzhou.cr.aliyuncs.com/higress"` | |
| controller.image | string | `"higress"` | |
| controller.imagePullSecrets | list | `[]` | |
| controller.labels | object | `{}` | |
| controller.name | string | `"higress-controller"` | |
| controller.nodeSelector | object | `{}` | |
| controller.podAnnotations | object | `{}` | |
| controller.podLabels | object | `{}` | 应用到 pod 上的标签 |
| controller.podSecurityContext | object | `{}` | |
| controller.ports[0].name | string | `"http"` | |
| controller.ports[0].port | int | `8888` | |
| controller.ports[0].protocol | string | `"TCP"` | |
| controller.ports[0].targetPort | int | `8888` | |
| controller.probe.httpGet.path | string | `"/ready"` | |
| controller.probe.httpGet.port | int | `8888` | |
| controller.probe.initialDelaySeconds | int | `1` | |
| controller.probe.periodSeconds | int | `3` | |
| controller.probe.timeoutSeconds | int | `5` | |
| controller.rbac.create | bool | `true` | |
| controller.replicas | int | `1` | Higress Controller pods 的数量 |
| controller.resources.limits.cpu | string | `"1000m"` | |
| controller.resources.limits.memory | string | `"2048Mi"` | |
| controller.resources.requests.cpu | string | `"500m"` | |
| controller.resources.requests.memory | string | `"2048Mi"` | |
| gateway.metrics.enabled | bool | `false` | 如果为 true则为gateway创建PodMonitor或VMPodScrape |
| gateway.metrics.provider | string | `monitoring.coreos.com` | CustomResourceDefinition 的提供商组名,可以是 monitoring.coreos.com 或 operator.victoriametrics.com |
| gateway.readinessFailureThreshold | int | `30` | 成功进行探针测试前连续失败探针的最大次数。 |
| global MeshNetworks | object | `{}` | |
| global.tracer.datadog.address | string | `"$(HOST_IP):8126"` | 提交给 Datadog agent 的 Host:Port 。|
| redis.redis.persistence.enabled | bool | `false` | 启用 Redis 持久性,默认为 false |
| redis.redis.persistence.size | string | `"1Gi"` | Persistent Volume 大小 |
| redis.redis.service.port | int | `6379` | Exporter service 端口 |
| tracing.skywalking.port | int | `11800` | |
| upstream.connectionBufferLimits | int | `10485760` | 上游连接缓冲限制(字节)|
| 键 | 类型 | 默认值 | 描述 |
|----|------|---------|-------------|
| clusterName | string | `""` | 集群名 |
| controller.affinity | object | `{}` | 控制器亲和性设置 |
| controller.automaticHttps.email | string | `""` | 自动 HTTPS 所需的邮件 |
| controller.automaticHttps.enabled | bool | `true` | 是否启用自动 HTTPS 功能 |
| controller.autoscaling.enabled | bool | `false` | 是否启用控制器的自动扩展功能 |
| controller.autoscaling.maxReplicas | int | `5` | 最大副本数 |
| controller.autoscaling.minReplicas | int | `1` | 最小副本数 |
| controller.autoscaling.targetCPUUtilizationPercentage | int | `80` | 目标 CPU 使用率百分比 |
| controller.env | object | `{}` | 环境变量 |
| controller.hub | string | `"higress-registry.cn-hangzhou.cr.aliyuncs.com/higress"` | 图像库的基础地址 |
| controller.image | string | `"higress"` | 镜像名称 |
| controller.imagePullSecrets | list | `[]` | 拉取秘钥列表 |
| controller.labels | object | `{}` | 标签 |
| controller.name | string | `"higress-controller"` | 控制器名称 |
| controller.nodeSelector | object | `{}` | 节点选择器 |
| controller.podAnnotations | object | `{}` | Pod 注解 |
| controller.podLabels | object | `{}` | 应用到 Pod 上的标签 |
| controller.podSecurityContext | object | `{}` | Pod 安全上下文 |
| controller.ports[0].name | string | `"http"` | 端口名称 |
| controller.ports[0].port | int | `8888` | 端口编号 |
| controller.ports[0].protocol | string | `"TCP"` | 协议类型 |
| controller.ports[0].targetPort | int | `8888` | 目标端口 |
| controller.ports[1].name | string | `"http-solver"` | 端口名称 |
| controller.ports[1].port | int | `8889` | 端口编号 |
| controller.ports[1].protocol | string | `"TCP"` | 协议类型 |
| controller.ports[1].targetPort | int | `8889` | 目标端口 |
| controller.ports[2].name | string | `"grpc"` | 端口名称 |
| controller.ports[2].port | int | `15051` | 端口编号 |
| controller.ports[2].protocol | string | `"TCP"` | 协议类型 |
| controller.ports[2].targetPort | int | `15051` | 目标端口 |
| controller.probe.httpGet.path | string | `"/ready"` | 运行状况检查路径 |
| controller.probe.httpGet.port | int | `8888` | 端口运行状态检查 |
| controller.probe.initialDelaySeconds | int | `1` | 初始延迟秒数 |
| controller.probe.periodSeconds | int | `3` | 健康检查间隔秒数 |
| controller.probe.timeoutSeconds | int | `5` | 超时秒数 |
| controller.rbac.create | bool | `true` | 是否创建 RBAC 相关资源 |
| controller.replicas | int | `1` | Higress 控制器 Pod 的数量 |
| controller.resources.limits.cpu | string | `"1000m"` | CPU 上限 |
| controller.resources.limits.memory | string | `"2048Mi"` | 内存上限 |
| controller.resources.requests.cpu | string | `"500m"` | CPU 请求量 |
| controller.resources.requests.memory | string | `"2048Mi"` | 内存请求量 |
| controller.securityContext | object | `{}` | 安全上下文 |
| controller.service.type | string | `"ClusterIP"` | 服务类型 |
| controller.serviceAccount.annotations | object | `{}` | 添加到服务帐户的注解 |
| controller.serviceAccount.create | bool | `true` | 是否创建服务帐户 |
| controller.serviceAccount.name | string | `""` | 如果未设置且 create 为 true则从 fullname 模板生成名称 |
| controller.tag | string | `""` | 标记 |
| controller.tolerations | list | `[]` | 受容容忍度列表 |
| downstream.connectionBufferLimits | int | `32768` | 下游连接缓冲区限制(字节) |
| downstream.http2.initialConnectionWindowSize | int | `1048576` | HTTP/2 初始连接窗口大小 |
| downstream.http2.initialStreamWindowSize | int | `65535` | 流初始窗口大小 |
| downstream.http2.maxConcurrentStreams | int | `100` | 并发流最大数量 |
| downstream.idleTimeout | int | `180` | 空闲超时时间(秒) |
| downstream.maxRequestHeadersKb | int | `60` | 最大请求头大小KB |
| downstream.routeTimeout | int | `0` | 路由超时时间 |
| gateway.affinity | object | `{}` | 网关的节点亲和性 |
| gateway.annotations | object | `{}` | 应用于所有资源的注解 |
| gateway.autoscaling.enabled | bool | `false` | 启用网关的自动扩展功能 |
| gateway.autoscaling.maxReplicas | int | `5` | 最大副本数 |
| gateway.autoscaling.minReplicas | int | `1` | 最小副本数 |
| gateway.autoscaling.targetCPUUtilizationPercentage | int | `80` | CPU 使用率的目标百分比 |
| gateway.containerSecurityContext | string | `nil` | 网关容器的安全配置上下文 |
| gateway.env | object | `{}` | Pod 环境变量 |
| gateway.hostNetwork | bool | `false` | 是否使用主机网络 |
| gateway.httpPort | int | `80` | HTTP 服务端口 |
| gateway.httpsPort | int | `443` | HTTPS 服务端口 |
| gateway.hub | string | `"higress-registry.cn-hangzhou.cr.aliyuncs.com/higress"` | 网关镜像的基础域名 |
| gateway.image | string | `"gateway"` | |
| gateway.kind | string | `"Deployment"` | 部署类型 |
| gateway.labels | object | `{}` | 应用于所有资源的标签 |
| gateway.metrics.enabled | bool | `false` | 启用网关度量收集 |
| gateway.metrics.honorLabels | bool | `false` | 是否合并现有标签 |
| gateway.metrics.interval | string | `""` | 度量间隔时间 |
| gateway.metrics.provider | string | `"monitoring.coreos.com"` | 定义监控提供者 |
| gateway.metrics.rawSpec | object | `{}` | 额外的度量规范 |
| gateway.metrics.relabelConfigs | list | `[]` | 重新标签配置 |
| gateway.metrics.relabelings | list | `[]` | 重新标签项 |
| gateway.metrics.scrapeTimeout | string | `""` | 抓取的超时时间 |
| gateway.name | string | `"higress-gateway"` | 网关名称 |
| gateway.networkGateway | string | `""` | 网络网关指定 |
| gateway.nodeSelector | object | `{}` | 节点选择器 |
| gateway.replicas | int | `2` | Higress Gateway pod 的数量 |
| gateway.resources.limits.cpu | string | `"2000m"` | 容器资源限制的 CPU |
| gateway.resources.limits.memory | string | `"2048Mi"` | 容器资源限制的内存 |
| gateway.resources.requests.cpu | string | `"2000m"` | 容器资源请求的 CPU |
| gateway.resources.requests.memory | string | `"2048Mi"` | 容器资源请求的内存 |
| gateway.revision | string | `""` | 网关所属版本声明 |
| gateway.rollingMaxSurge | string | `"100%"` | 最大激增数目百分比 |
| gateway.rollingMaxUnavailable | string | `"25%"` | 最大不可用比例 |
| gateway.readinessFailureThreshold | int | `30` | 成功尝试之前连续失败的最大探测次数 |
| gateway.readinessInitialDelaySeconds | int | `1` | 初次检测推迟多少秒后开始探测存活状态 |
| gateway.readinessPeriodSeconds | int | `2` | 存活探测间隔秒数 |
| gateway.readinessSuccessThreshold | int | `1` | 认为成功之前连续成功最小探测次数 |
| gateway.readinessTimeoutSeconds | int | `3` | 存活探测超时秒数 |
| gateway.securityContext | string | `nil` | 客户豆荚的安全上下文 |
| gateway.service.annotations | object | `{}` | 应用于服务账户的注释 |
| gateway.service.externalTrafficPolicy | string | `""` | 外部路由策略 |
| gateway.service.loadBalancerClass | string | `""` | 负载均衡器类别 |
| gateway.service.loadBalancerIP | string | `""` | 负载均衡器 IP 地址 |
| gateway.service.loadBalancerSourceRanges | list | `[]` | 允许访问负载均衡器的 CIDR 范围 |
| gateway.service.ports[0].name | string | `"http2"` | 服务定义的端口名称 |
| gateway.service.ports[0].port | int | `80` | 服务端口 |
| gateway.service.ports[0].protocol | string | `"TCP"` | 协议 |
| gateway.service.ports[0].targetPort | int | `80` | 靶向端口 |
| gateway.service.ports[1].name | string | `"https"` | 服务定义的端口名称 |
| gateway.service.ports[1].port | int | `443` | 服务端口 |
| gateway.service.ports[1].protocol | string | `"TCP"` | 协议 |
| gateway.service.ports[1].targetPort | int | `443` | 靶向端口 |
| gateway.service.type | string | `"LoadBalancer"` | 服务类型 |
| global.disableAlpnH2 | bool | `false` | 设置是否禁用 ALPN 中的 http/2 |
| ... | ... | ... | ... |
由于内容较多,其他参数可以参考完整表。

View File

@@ -56,7 +56,7 @@ type MCPRatelimitConfig struct {
type SSEServer struct {
// The name of the SSE server
Name string `json:"name,omitempty"`
// The path where the SSE server will be mounted, the full path is (PATH + SsePathSuffix)
// The path where the SSE server will be mounted, the full path is (PATH + SSEPathSuffix)
Path string `json:"path,omitempty"`
// The type of the SSE server
Type string `json:"type,omitempty"`
@@ -74,6 +74,12 @@ type MatchRule struct {
MatchRulePath string `json:"match_rule_path,omitempty"`
// Type of match rule: exact, prefix, suffix, contains, regex
MatchRuleType string `json:"match_rule_type,omitempty"`
// Type of upstream(s) matched by the rule: rest (default), sse
UpstreamType string `json:"upstream_type"`
// Enable request path rewrite for matched routes
EnablePathRewrite bool `json:"enable_path_rewrite"`
// Prefix the request path would be rewritten to.
PathRewritePrefix string `json:"path_rewrite_prefix"`
}
// McpServer defines the configuration for MCP (Model Context Protocol) server
@@ -83,7 +89,7 @@ type McpServer struct {
// Redis Config for MCP server
Redis *RedisConfig `json:"redis,omitempty"`
// The suffix to be appended to SSE paths, default is "/sse"
SsePathSuffix string `json:"sse_path_suffix,omitempty"`
SSEPathSuffix string `json:"sse_path_suffix,omitempty"`
// List of SSE servers Configs
Servers []*SSEServer `json:"servers,omitempty"`
// List of match rules for filtering requests
@@ -118,21 +124,32 @@ func validMcpServer(m *McpServer) error {
// Validate match rule types
if m.MatchList != nil {
validTypes := map[string]bool{
validMatchRuleTypes := map[string]bool{
"exact": true,
"prefix": true,
"suffix": true,
"contains": true,
"regex": true,
}
validUpstreamTypes := map[string]bool{
"rest": true,
"sse": true,
"streamable": true,
}
for _, rule := range m.MatchList {
if rule.MatchRuleType == "" {
return errors.New("match_rule_type cannot be empty, must be one of: exact, prefix, suffix, contains, regex")
}
if !validTypes[rule.MatchRuleType] {
if !validMatchRuleTypes[rule.MatchRuleType] {
return fmt.Errorf("invalid match_rule_type: %s, must be one of: exact, prefix, suffix, contains, regex", rule.MatchRuleType)
}
if rule.UpstreamType != "" && !validUpstreamTypes[rule.UpstreamType] {
return fmt.Errorf("invalid upstream_type: %s, must be one of: rest, sse, streamable", rule.UpstreamType)
}
if rule.EnablePathRewrite && rule.UpstreamType != "sse" {
return errors.New("path rewrite is only supported for SSE upstream type")
}
}
}
@@ -174,7 +191,7 @@ func deepCopyMcpServer(mcp *McpServer) (*McpServer, error) {
WhiteList: mcp.Ratelimit.WhiteList,
}
}
newMcp.SsePathSuffix = mcp.SsePathSuffix
newMcp.SSEPathSuffix = mcp.SSEPathSuffix
newMcp.EnableUserLevelServer = mcp.EnableUserLevelServer
@@ -201,9 +218,12 @@ func deepCopyMcpServer(mcp *McpServer) (*McpServer, error) {
newMcp.MatchList = make([]*MatchRule, len(mcp.MatchList))
for i, rule := range mcp.MatchList {
newMcp.MatchList[i] = &MatchRule{
MatchRuleDomain: rule.MatchRuleDomain,
MatchRulePath: rule.MatchRulePath,
MatchRuleType: rule.MatchRuleType,
MatchRuleDomain: rule.MatchRuleDomain,
MatchRulePath: rule.MatchRulePath,
MatchRuleType: rule.MatchRuleType,
UpstreamType: rule.UpstreamType,
EnablePathRewrite: rule.EnablePathRewrite,
PathRewritePrefix: rule.PathRewritePrefix,
}
}
}
@@ -216,7 +236,7 @@ type McpServerController struct {
mcpServer atomic.Value
Name string
eventHandler ItemEventHandler
reconclier *reconcile.Reconciler
reconciler *reconcile.Reconciler
}
func NewMcpServerController(namespace string) *McpServerController {
@@ -291,7 +311,7 @@ func (m *McpServerController) RegisterItemEventHandler(eventHandler ItemEventHan
}
func (m *McpServerController) RegisterMcpReconciler(reconciler *reconcile.Reconciler) {
m.reconclier = reconciler
m.reconciler = reconciler
}
func (m *McpServerController) ConstructEnvoyFilters() ([]*config.Config, error) {
@@ -393,13 +413,16 @@ func (m *McpServerController) constructMcpSessionStruct(mcp *McpServer) string {
matchConfigs = append(matchConfigs, fmt.Sprintf(`{
"match_rule_domain": "%s",
"match_rule_path": "%s",
"match_rule_type": "%s"
}`, rule.MatchRuleDomain, rule.MatchRulePath, rule.MatchRuleType))
"match_rule_type": "%s",
"upstream_type": "%s",
"enable_path_rewrite": %t,
"path_rewrite_prefix": "%s"
}`, rule.MatchRuleDomain, rule.MatchRulePath, rule.MatchRuleType, rule.UpstreamType, rule.EnablePathRewrite, rule.PathRewritePrefix))
}
}
if m.reconclier != nil {
vsFromMcp := m.reconclier.GetAllConfigs(gvk.VirtualService)
if m.reconciler != nil {
vsFromMcp := m.reconciler.GetAllConfigs(gvk.VirtualService)
for _, c := range vsFromMcp {
vs := c.Spec.(*networking.VirtualService)
var host string
@@ -468,7 +491,7 @@ func (m *McpServerController) constructMcpSessionStruct(mcp *McpServer) string {
}`,
redisConfig,
rateLimitConfig,
mcp.SsePathSuffix,
mcp.SSEPathSuffix,
matchList,
mcp.EnableUserLevelServer)
}

View File

@@ -54,6 +54,61 @@ func Test_validMcpServer(t *testing.T) {
},
wantErr: nil,
},
{
name: "enabled but bad match_rule_type",
mcp: &McpServer{
Enable: true,
EnableUserLevelServer: false,
Redis: nil,
MatchList: []*MatchRule{
{
MatchRuleDomain: "*",
MatchRulePath: "/mcp",
MatchRuleType: "bad-type",
},
},
Servers: []*SSEServer{},
},
wantErr: errors.New("invalid match_rule_type: bad-type, must be one of: exact, prefix, suffix, contains, regex"),
},
{
name: "enabled but bad upstream_type",
mcp: &McpServer{
Enable: true,
EnableUserLevelServer: false,
Redis: nil,
MatchList: []*MatchRule{
{
MatchRuleDomain: "*",
MatchRulePath: "/mcp",
MatchRuleType: "prefix",
UpstreamType: "bad-type",
},
},
Servers: []*SSEServer{},
},
wantErr: errors.New("invalid upstream_type: bad-type, must be one of: rest, sse, streamable"),
},
{
name: "enabled but path rewrite with unsupported upstream type",
mcp: &McpServer{
Enable: true,
EnableUserLevelServer: false,
Redis: nil,
MatchList: []*MatchRule{
{
MatchRuleDomain: "*",
MatchRulePath: "/mcp",
MatchRuleType: "prefix",
UpstreamType: "rest",
EnablePathRewrite: true,
PathRewritePrefix: "/",
},
},
Servers: []*SSEServer{},
},
wantErr: errors.New("path rewrite is only supported for SSE upstream type"),
},
{
name: "enabled with user level server but no redis config",
mcp: &McpServer{
@@ -76,7 +131,7 @@ func Test_validMcpServer(t *testing.T) {
Password: "password",
DB: 0,
},
SsePathSuffix: "/sse",
SSEPathSuffix: "/sse",
MatchList: []*MatchRule{
{
MatchRuleDomain: "*",
@@ -238,7 +293,7 @@ func Test_deepCopyMcpServer(t *testing.T) {
Password: "password",
DB: 0,
},
SsePathSuffix: "/sse",
SSEPathSuffix: "/sse",
MatchList: []*MatchRule{
{
MatchRuleDomain: "*",
@@ -265,7 +320,7 @@ func Test_deepCopyMcpServer(t *testing.T) {
Password: "password",
DB: 0,
},
SsePathSuffix: "/sse",
SSEPathSuffix: "/sse",
MatchList: []*MatchRule{
{
MatchRuleDomain: "*",
@@ -581,13 +636,27 @@ func TestMcpServerController_constructMcpSessionStruct(t *testing.T) {
Password: "pass",
DB: 1,
},
SsePathSuffix: "/sse",
SSEPathSuffix: "/sse",
MatchList: []*MatchRule{
{
MatchRuleDomain: "*",
MatchRulePath: "/test",
MatchRuleType: "exact",
},
{
MatchRuleDomain: "*",
MatchRulePath: "/sse-test-1",
MatchRuleType: "prefix",
UpstreamType: "sse",
},
{
MatchRuleDomain: "*",
MatchRulePath: "/sse-test-2",
MatchRuleType: "prefix",
UpstreamType: "sse",
EnablePathRewrite: true,
PathRewritePrefix: "/mcp",
},
},
EnableUserLevelServer: true,
Ratelimit: &MCPRatelimitConfig{
@@ -623,7 +692,24 @@ func TestMcpServerController_constructMcpSessionStruct(t *testing.T) {
"match_list": [{
"match_rule_domain": "*",
"match_rule_path": "/test",
"match_rule_type": "exact"
"match_rule_type": "exact",
"upstream_type": "",
"enable_path_rewrite": false,
"path_rewrite_prefix": ""
},{
"match_rule_domain": "*",
"match_rule_path": "/sse-test-1",
"match_rule_type": "prefix",
"upstream_type": "sse",
"enable_path_rewrite": false,
"path_rewrite_prefix": ""
},{
"match_rule_domain": "*",
"match_rule_path": "/sse-test-2",
"match_rule_type": "prefix",
"upstream_type": "sse",
"enable_path_rewrite": true,
"path_rewrite_prefix": "/mcp"
}],
"enable_user_level_server": true
}

View File

@@ -20,24 +20,38 @@ Golang HTTP Filter 允许开发者使用 Go 语言编写自定义的 Envoy Filte
请参考 [Envoy Golang HTTP Filter 示例](https://github.com/envoyproxy/examples/tree/main/golang-http) 了解如何开发和运行一个基本的 Golang Filter。
## 插件注册
在开发新的 Golang Filter 时,需要在`main.go``init()` 函数中注册你的插件。注册时需要提供插件名称、Filter 工厂函数和配置解析器:
```go
func init() {
envoyHttp.RegisterHttpFilterFactoryAndConfigParser(
"your-plugin-name", // 插件名称
yourFilterFactory, // Filter 工厂函数
&yourConfigParser{}, // 配置解析器
)
}
```
## 配置示例
多个 Golang Filter 插件可以共同编译到一个 `golang-filter.so` 文件中,通过 `plugin_name` 来指定要使用的插件。配置示例如下:
```yaml
http_filters:
- name: envoy.filters.http.golang
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config
library_id: my-go-filter
library_path: "./go-filter.so"
plugin_name: my-go-filter
library_id: your-plugin-name
library_path: "./golang-filter.so" # 包含多个插件的共享库文件
plugin_name: your-plugin-name # 指定要使用的插件名称,需要与 init() 函数中注册的插件名称保持一致
plugin_config:
"@type": type.googleapis.com/xds.type.v3.TypedStruct
value:
your_config_here: value
```
## 快速构建
使用以下命令可以快速构建 golang filter 插件:

View File

@@ -20,16 +20,32 @@ The Golang HTTP Filter allows developers to write custom Envoy Filters using the
Please refer to [Envoy Golang HTTP Filter Example](https://github.com/envoyproxy/examples/tree/main/golang-http) to learn how to develop and run a basic Golang Filter.
## Plugin Registration
When developing a new Golang Filter, you need to register your plugin in the `init()` function of `main.go`. The registration requires a plugin name, Filter factory function, and configuration parser:
```go
func init() {
envoyHttp.RegisterHttpFilterFactoryAndConfigParser(
"your-plugin-name", // Plugin name
yourFilterFactory, // Filter factory function
&yourConfigParser{}, // Configuration parser
)
}
```
## Configuration Example
Multiple Golang Filter plugins can be compiled into a single `golang-filter.so` file, and the desired plugin can be specified using `plugin_name`. Here's an example configuration:
```yaml
http_filters:
- name: envoy.filters.http.golang
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config
library_id: my-go-filter
library_path: "./my-go-filter.so"
plugin_name: my-go-filter
library_id: your-plugin-name
library_path: "./golang-filter.so" # Shared library file containing multiple plugins
plugin_name: your-plugin-name # Specify which plugin to use, must match the name registered in init()
plugin_config:
"@type": type.googleapis.com/xds.type.v3.TypedStruct
value:
@@ -41,5 +57,5 @@ http_filters:
Use the following command to quickly build the golang filter plugin:
```bash
GO_FILTER_NAME=mcp-server make build
make build
```

View File

@@ -2,7 +2,7 @@ module github.com/alibaba/higress/plugins/golang-filter
go 1.22
replace github.com/envoyproxy/envoy => github.com/higress-group/envoy v0.0.0-20250428030521-17cf01d9f644
replace github.com/envoyproxy/envoy => github.com/higress-group/envoy v0.0.0-20250430151331-2c556780b65c
replace github.com/mark3labs/mcp-go => github.com/higress-group/mcp-go v0.0.0-20250428145706-792ce64b4b30

View File

@@ -234,8 +234,8 @@ github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mO
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/higress-group/envoy v0.0.0-20250428030521-17cf01d9f644 h1:wiLDdiOT3BcTQSFs8oTMu54GIiPFSwKLuWo5J0Cd9b8=
github.com/higress-group/envoy v0.0.0-20250428030521-17cf01d9f644/go.mod h1:SU+IJUAfh1kkZtH+u0E1dnwho8AhbGeYMgp5vvjU+Gc=
github.com/higress-group/envoy v0.0.0-20250430151331-2c556780b65c h1:chAOZk/qEXFhLILWoNucj3X6r9xYnRR+SWFvhsOa2oo=
github.com/higress-group/envoy v0.0.0-20250430151331-2c556780b65c/go.mod h1:SU+IJUAfh1kkZtH+u0E1dnwho8AhbGeYMgp5vvjU+Gc=
github.com/higress-group/mcp-go v0.0.0-20250428145706-792ce64b4b30 h1:N4NMq8M1nZyyChPyzn+EUUdHi5asig2uLR5hOyRmsXI=
github.com/higress-group/mcp-go v0.0.0-20250428145706-792ce64b4b30/go.mod h1:O9gri9UOzthw728vusc2oNu99lVh8cKCajpxNfC90gE=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=

View File

@@ -3,27 +3,22 @@
## 概述
MCP Server 是一个基于 Envoy 的 Golang Filter 插件,用于实现服务器端事件SSE和消息通信功能。该插件支持多种数据库类型并使用 Redis 作为消息队列来实现负载均衡的请求通过对应的SSE连接发送。
MCP Server 是一个基于 Envoy 的 Golang Filter 插件,提供了统一的 MCP (Model Context Protocol) 服务接口。它支持多种后端服务的集成,包括:
> **注意**MCP Server需要 Higress 2.1.0 或更高版本才能使用。
## 项目结构
```
mcp-server/
├── config.go # 配置解析相关代码
├── filter.go # 请求处理相关代码
├── internal/ # 内部实现逻辑
├── servers/ # MCP 服务器实现
├── go.mod # Go模块依赖定义
└── go.sum # Go模块依赖校验
```
## MCP Server开发指南
- 数据库服务:通过 GORM 支持多种数据库的访问和管理
- 配置中心:支持 Nacos 配置中心的集成
- 可扩展性:支持自定义服务器实现,方便集成其他服务
> **注意**MCP Server 需要 Higress 2.1.0 或更高版本才能使用。
## MCP Server 开发指南
```go
// 在init函数中注册你的服务器
// 参数1: 服务器名称
// 参数2: 配置结构体实例
func init() {
internal.GlobalRegistry.RegisterServer("demo", &DemoConfig{})
common.GlobalRegistry.RegisterServer("demo", &DemoConfig{})
}
// 服务器配置结构体
@@ -43,8 +38,8 @@ func (c *DBConfig) ParseConfig(config map[string]any) error {
// 创建新的MCP服务器实例
// serverName: 服务器名称
// 返回值: MCP服务器实例和可能的错误
func (c *DBConfig) NewServer(serverName string) (*internal.MCPServer, error) {
mcpServer := internal.NewMCPServer(serverName, Version)
func (c *DBConfig) NewServer(serverName string) (*common.MCPServer, error) {
mcpServer := common.NewMCPServer(serverName, Version)
// 添加工具方法到服务器
// mcpServer.AddTool()

View File

@@ -0,0 +1,60 @@
# MCP Server
English | [简体中文](./README.md)
## Overview
MCP Server is a Golang Filter plugin based on Envoy that provides a unified MCP (Model Context Protocol) service interface. It supports integration with various backend services, including:
- Database Services: Supports multiple database access and management through GORM
- Configuration Service: Supports integration with Nacos configuration service
- Extensibility: Supports custom server implementations for easy integration with other services
> **Note**: MCP Server requires Higress version 2.1.0 or higher to be used.
## MCP Server Development Guide
```go
// Register your server in the init function
// Parameter 1: Server name
// Parameter 2: Configuration struct instance
func init() {
common.GlobalRegistry.RegisterServer("demo", &DemoConfig{})
}
// Server configuration struct
type DemoConfig struct {
helloworld string
}
// Parse configuration method
// Parse and validate configuration items from the config map
func (c *DBConfig) ParseConfig(config map[string]any) error {
helloworld, ok := config["helloworld"].(string)
if !ok { return errors.New("missing helloworld")}
c.helloworld = helloworld
return nil
}
// Create a new MCP server instance
// serverName: Server name
// Returns: MCP server instance and possible error
func (c *DBConfig) NewServer(serverName string) (*common.MCPServer, error) {
mcpServer := common.NewMCPServer(serverName, Version)
// Add tool methods to the server
// mcpServer.AddTool()
// Add resources to the server
// mcpServer.AddResource()
return mcpServer, nil
}
```
**Note**:
You need to use underscore imports in config.go to execute the package's init function
```go
import (
_ "github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/gorm"
)
```

View File

@@ -1,67 +0,0 @@
# MCP Server
English | [简体中文](./README.md)
## Overview
MCP Server is a Golang Filter plugin based on Envoy, designed to implement Server-Sent Events (SSE) and message communication functionality. This plugin supports various database types and uses Redis as a message queue to enable load-balanced requests to be sent through corresponding SSE connections.
> **Note**: MCP Server requires Higress 2.1.0 or higher version.
## Project Structure
```
mcp-server/
├── config.go # Configuration parsing code
├── filter.go # Request processing code
├── internal/ # Internal implementation logic
├── servers/ # MCP server implementation
├── go.mod # Go module dependency definition
└── go.sum # Go module dependency checksum
```
## MCP Server Development Guide
```go
// Register your server in the init function
// Param 1: Server name
// Param 2: Config struct instance
func init() {
internal.GlobalRegistry.RegisterServer("demo", &DemoConfig{})
}
// Server configuration struct
type DemoConfig struct {
helloworld string
}
// Configuration parsing method
// Parse and validate configuration items from the config map
func (c *DBConfig) ParseConfig(config map[string]any) error {
helloworld, ok := config["helloworld"].(string)
if !ok { return errors.New("missing helloworld")}
c.helloworld = helloworld
return nil
}
// Create a new MCP server instance
// serverName: Server name
// Returns: MCP server instance and possible error
func (c *DBConfig) NewServer(serverName string) (*internal.MCPServer, error) {
mcpServer := internal.NewMCPServer(serverName, Version)
// Add tool methods to server
// mcpServer.AddTool()
// Add resources to server
// mcpServer.AddResource()
return mcpServer, nil
}
```
**Note**:
Need to use underscore import in config.go to execute the package's init function
```go
import (
_ "github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/gorm"
)
```

View File

@@ -3,24 +3,36 @@ package common
import (
"regexp"
"strings"
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
)
// RuleType defines the type of matching rule
type RuleType string
// UpstreamType defines the type of matching rule
type UpstreamType string
const (
ExactMatch RuleType = "exact"
PrefixMatch RuleType = "prefix"
SuffixMatch RuleType = "suffix"
ContainsMatch RuleType = "contains"
RegexMatch RuleType = "regex"
RestUpstream UpstreamType = "rest"
SSEUpstream UpstreamType = "sse"
StreamableUpstream UpstreamType = "streamable"
)
// MatchRule defines the structure for a matching rule
type MatchRule struct {
MatchRuleDomain string `json:"match_rule_domain"` // Domain pattern, supports wildcards
MatchRulePath string `json:"match_rule_path"` // Path pattern to match
MatchRuleType RuleType `json:"match_rule_type"` // Type of match rule
MatchRuleDomain string `json:"match_rule_domain"` // Domain pattern, supports wildcards
MatchRulePath string `json:"match_rule_path"` // Path pattern to match
MatchRuleType RuleType `json:"match_rule_type"` // Type of match rule
UpstreamType UpstreamType `json:"upstream_type"` // Type of upstream(s) matched by the rule
EnablePathRewrite bool `json:"enable_path_rewrite"` // Enable request path rewrite for matched routes
PathRewritePrefix string `json:"path_rewrite_prefix"` // Prefix the request path would be rewritten to.
}
// ParseMatchList parses the match list from the config
@@ -38,6 +50,34 @@ func ParseMatchList(matchListConfig []interface{}) []MatchRule {
if ruleType, ok := ruleMap["match_rule_type"].(string); ok {
rule.MatchRuleType = RuleType(ruleType)
}
if upstreamType, ok := ruleMap["upstream_type"].(string); ok {
rule.UpstreamType = UpstreamType(upstreamType)
}
if len(rule.UpstreamType) == 0 {
rule.UpstreamType = RestUpstream
} else {
switch rule.UpstreamType {
case RestUpstream, SSEUpstream, StreamableUpstream:
break
default:
api.LogWarnf("Unknown upstream type: %s", rule.UpstreamType)
}
}
if enablePathRewrite, ok := ruleMap["enable_path_rewrite"].(bool); ok {
rule.EnablePathRewrite = enablePathRewrite
}
if pathRewritePrefix, ok := ruleMap["path_rewrite_prefix"].(string); ok {
rule.PathRewritePrefix = pathRewritePrefix
}
if rule.EnablePathRewrite {
if rule.UpstreamType != SSEUpstream {
api.LogWarnf("Path rewrite is only supported for SSE upstream type")
} else if rule.MatchRuleType != PrefixMatch {
api.LogWarnf("Path rewrite is only supported for prefix match type")
} else if !strings.HasPrefix(rule.PathRewritePrefix, "/") {
rule.PathRewritePrefix = "/" + rule.PathRewritePrefix
}
}
matchList = append(matchList, rule)
}
}
@@ -96,17 +136,17 @@ func matchDomainAndPath(domain, path string, rule MatchRule) bool {
// IsMatch checks if the request matches any rule in the rule list
// Returns true if no rules are specified
func IsMatch(rules []MatchRule, host, path string) bool {
func IsMatch(rules []MatchRule, host, path string) (bool, MatchRule) {
if len(rules) == 0 {
return true
return true, MatchRule{}
}
for _, rule := range rules {
if matchDomainAndPath(host, path, rule) {
return true
return true, rule
}
}
return false
return false, MatchRule{}
}
// MatchDomainList checks if the domain matches any of the domains in the list

View File

@@ -2,6 +2,7 @@ package mcp_session
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
@@ -28,10 +29,14 @@ type filter struct {
config *config
stopChan chan struct{}
req *http.Request
serverName string
proxyURL *url.URL
neepProcess bool
req *http.Request
serverName string
proxyURL *url.URL
matchedRule common.MatchRule
needProcess bool
skipRequestBody bool
skipResponseBody bool
cachedResponseBody []byte
userLevelConfig bool
mcpConfigHandler *handler.MCPConfigHandler
@@ -42,31 +47,33 @@ type filter struct {
// Callbacks which are called in request path
// The endStream is true if the request doesn't have body
func (f *filter) DecodeHeaders(header api.RequestHeaderMap, endStream bool) api.StatusType {
url := common.NewRequestURL(header)
if url == nil {
requestUrl := common.NewRequestURL(header)
if requestUrl == nil {
return api.Continue
}
f.path = url.ParsedURL.Path
f.path = requestUrl.ParsedURL.Path
// Check if request matches any rule in match_list
if !common.IsMatch(f.config.matchList, url.Host, f.path) {
api.LogDebugf("Request does not match any rule in match_list: %s", url.ParsedURL.String())
matched, matchedRule := common.IsMatch(f.config.matchList, requestUrl.Host, f.path)
if !matched {
api.LogDebugf("Request does not match any rule in match_list: %s", requestUrl.ParsedURL.String())
return api.Continue
}
f.neepProcess = true
f.needProcess = true
f.matchedRule = matchedRule
f.req = &http.Request{
Method: url.Method,
URL: url.ParsedURL,
Method: requestUrl.Method,
URL: requestUrl.ParsedURL,
}
if strings.HasSuffix(f.path, ConfigPathSuffix) && f.config.enableUserLevelServer {
if !url.InternalIP {
api.LogWarnf("Access denied: non-Internal IP address %s", url.ParsedURL.String())
if !requestUrl.InternalIP {
api.LogWarnf("Access denied: non-Internal IP address %s", requestUrl.ParsedURL.String())
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusForbidden, "", nil, 0, "")
return api.LocalReply
}
if strings.HasSuffix(f.path, ConfigPathSuffix) && url.Method == http.MethodGet {
if strings.HasSuffix(f.path, ConfigPathSuffix) && requestUrl.Method == http.MethodGet {
api.LogDebugf("Handling config request: %s", f.path)
f.mcpConfigHandler.HandleConfigRequest(f.req, []byte{})
return api.LocalReply
@@ -79,10 +86,27 @@ func (f *filter) DecodeHeaders(header api.RequestHeaderMap, endStream bool) api.
}
}
if !strings.HasSuffix(url.ParsedURL.Path, GlobalSSEPathSuffix) {
f.proxyURL = url.ParsedURL
return f.processMcpRequestHeaders(header, endStream)
}
func (f *filter) processMcpRequestHeaders(header api.RequestHeaderMap, endStream bool) api.StatusType {
switch f.matchedRule.UpstreamType {
case common.RestUpstream, common.StreamableUpstream:
return f.processMcpRequestHeadersForRestUpstream(header, endStream)
case common.SSEUpstream:
return f.processMcpRequestHeadersForSSEUpstream(header, endStream)
}
f.needProcess = false
return api.Continue
}
func (f *filter) processMcpRequestHeadersForRestUpstream(header api.RequestHeaderMap, endStream bool) api.StatusType {
method := f.req.Method
requestUrl := f.req.URL
if !strings.HasSuffix(requestUrl.Path, GlobalSSEPathSuffix) {
f.proxyURL = requestUrl
if f.config.enableUserLevelServer {
parts := strings.Split(url.ParsedURL.Path, "/")
parts := strings.Split(requestUrl.Path, "/")
if len(parts) >= 3 {
serverName := parts[1]
uid := parts[2]
@@ -102,12 +126,12 @@ func (f *filter) DecodeHeaders(header api.RequestHeaderMap, endStream bool) api.
}
}
if url.Method != http.MethodGet {
if method != http.MethodGet {
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusMethodNotAllowed, "Method not allowed", nil, 0, "")
} else {
f.config.defaultServer = common.NewSSEServer(common.NewMCPServer(DefaultServerName, Version),
common.WithSSEEndpoint(GlobalSSEPathSuffix),
common.WithMessageEndpoint(strings.TrimSuffix(url.ParsedURL.Path, GlobalSSEPathSuffix)),
common.WithMessageEndpoint(strings.TrimSuffix(requestUrl.Path, GlobalSSEPathSuffix)),
common.WithRedisClient(f.config.redisClient))
f.serverName = f.config.defaultServer.GetServerName()
body := "SSE connection create"
@@ -116,10 +140,60 @@ func (f *filter) DecodeHeaders(header api.RequestHeaderMap, endStream bool) api.
return api.LocalReply
}
func (f *filter) processMcpRequestHeadersForSSEUpstream(header api.RequestHeaderMap, endStream bool) api.StatusType {
// We don't need to process the request body for SSE upstream.
f.skipRequestBody = true
f.rewritePathForSSEUpstream(header)
return api.Continue
}
func (f *filter) rewritePathForSSEUpstream(header api.RequestHeaderMap) {
matchedRule := f.matchedRule
if !matchedRule.EnablePathRewrite || matchedRule.MatchRuleType != common.PrefixMatch {
// No rewrite required, so we don't need to process the response body, either.
f.skipResponseBody = true
return
}
path := f.req.URL.Path
if !strings.HasPrefix(path, matchedRule.MatchRulePath) {
api.LogWarnf("Unexpected: Path %s does not match the configured prefix %s", path, matchedRule.MatchRulePath)
return
}
rewrittenPath := path[len(matchedRule.MatchRulePath):]
if rewrittenPath == "" {
rewrittenPath = matchedRule.PathRewritePrefix
} else {
rewritePrefixHasTrailingSlash := strings.HasSuffix(matchedRule.PathRewritePrefix, "/")
pathSuffixHasLeadingSlash := strings.HasPrefix(rewrittenPath, "/")
if rewritePrefixHasTrailingSlash != pathSuffixHasLeadingSlash {
// One has, the other doesn't have.
rewrittenPath = matchedRule.PathRewritePrefix + rewrittenPath
} else if pathSuffixHasLeadingSlash {
// Both have.
rewrittenPath = matchedRule.PathRewritePrefix + rewrittenPath[1:]
} else {
// Neither have.
rewrittenPath = matchedRule.PathRewritePrefix + "/" + rewrittenPath
}
}
if f.req.URL.RawQuery != "" {
rewrittenPath = rewrittenPath + "?" + f.req.URL.RawQuery
}
header.SetPath(rewrittenPath)
}
// DecodeData might be called multiple times during handling the request body.
// The endStream is true when handling the last piece of the body.
func (f *filter) DecodeData(buffer api.BufferInstance, endStream bool) api.StatusType {
if !f.neepProcess {
if !f.needProcess || f.skipRequestBody {
return api.Continue
}
if f.matchedRule.UpstreamType != common.RestUpstream && f.matchedRule.UpstreamType != common.StreamableUpstream {
return api.Continue
}
if !endStream {
@@ -158,10 +232,17 @@ func (f *filter) DecodeData(buffer api.BufferInstance, endStream bool) api.Statu
return api.Continue
}
// Callbacks which are called in response path
// The endStream is true if the response doesn't have body
// EncodeHeaders Callbacks which are called in response path.
// The endStream is true if the response doesn't have body.
func (f *filter) EncodeHeaders(header api.ResponseHeaderMap, endStream bool) api.StatusType {
if !f.neepProcess {
if !f.needProcess {
return api.Continue
}
if f.matchedRule.UpstreamType != common.RestUpstream && f.matchedRule.UpstreamType != common.StreamableUpstream {
if contentType, ok := header.Get("content-type"); !ok || !strings.HasPrefix(contentType, "text/event-stream") {
api.LogDebugf("Skip response body for non-SSE upstream. Content-Type: %s", contentType)
f.skipResponseBody = true
}
return api.Continue
}
if f.serverName != "" {
@@ -182,7 +263,30 @@ func (f *filter) EncodeHeaders(header api.ResponseHeaderMap, endStream bool) api
// EncodeData might be called multiple times during handling the response body.
// The endStream is true when handling the last piece of the body.
func (f *filter) EncodeData(buffer api.BufferInstance, endStream bool) api.StatusType {
if !f.neepProcess {
if !f.needProcess || f.skipResponseBody {
return api.Continue
}
ret := api.Continue
api.LogDebugf("Upstream Type: %s", f.matchedRule.UpstreamType)
switch f.matchedRule.UpstreamType {
case common.RestUpstream, common.StreamableUpstream:
api.LogDebugf("Encoding data from Rest upstream")
ret = f.encodeDataFromRestUpstream(buffer, endStream)
break
case common.SSEUpstream:
api.LogDebugf("Encoding data from SSE upstream")
ret = f.encodeDataFromSSEUpstream(buffer, endStream)
if endStream {
// Always continue as long as the stream has ended.
ret = api.Continue
}
}
return ret
}
func (f *filter) encodeDataFromRestUpstream(buffer api.BufferInstance, endStream bool) api.StatusType {
if !f.needProcess {
return api.Continue
}
if !endStream {
@@ -207,13 +311,157 @@ func (f *filter) EncodeData(buffer api.BufferInstance, endStream bool) api.Statu
f.config.defaultServer.HandleSSE(f.callbacks, f.stopChan)
return api.Running
} else {
buffer.SetString(RedisNotEnabledResponseBody)
_ = buffer.SetString(RedisNotEnabledResponseBody)
return api.Continue
}
}
return api.Continue
}
func (f *filter) encodeDataFromSSEUpstream(buffer api.BufferInstance, endStream bool) api.StatusType {
bufferBytes := buffer.Bytes()
bufferData := string(bufferBytes)
err, lineBreak := f.findSSELineBreak(bufferData)
if err != nil {
api.LogWarnf("Failed to find line break in SSE data: %v", err)
f.needProcess = false
return api.Continue
}
if lineBreak == "" {
// Have not found any line break. Need to buffer and check again.
return api.StopAndBuffer
}
api.LogDebugf("Line break sequence: %v", []byte(lineBreak))
err, endpointUrl := f.findEndpointUrl(bufferData, lineBreak)
if err != nil {
api.LogWarnf("Failed to find endpoint URL in SSE data: %v", err)
f.needProcess = false
return api.Continue
}
if endpointUrl == "" {
// No endpoint URL found. Need to buffer and check again.
return api.StopAndBuffer
}
// Remove query string since we don't need to change it.
queryStringIndex := strings.IndexAny(endpointUrl, "?")
if queryStringIndex != -1 {
endpointUrl = endpointUrl[:queryStringIndex]
}
if changed, newEndpointUrl := f.rewriteEndpointUrl(endpointUrl); changed {
api.LogDebugf("The endpoint URL is changed.\n Old: %s\n New: %s", endpointUrl, newEndpointUrl)
endpointUrlIndex := strings.Index(bufferData, endpointUrl)
if endpointUrlIndex == -1 {
api.LogWarnf("Something wrong, the previously found endpoint URL %s not found in the SSE data now", endpointUrl)
} else {
bufferData = bufferData[:endpointUrlIndex] + newEndpointUrl + bufferData[endpointUrlIndex+len(endpointUrl):]
_ = buffer.SetString(bufferData)
}
} else {
api.LogDebugf("The endpoint URL %s is not changed", endpointUrl)
}
f.needProcess = false
return api.Continue
}
func (f *filter) rewriteEndpointUrl(endpointUrl string) (bool, string) {
if !f.matchedRule.EnablePathRewrite {
return false, ""
}
if schemeIndex := strings.Index(endpointUrl, "://"); schemeIndex != -1 {
endpointUrl = endpointUrl[schemeIndex+3:]
if slashIndex := strings.Index(endpointUrl, "/"); slashIndex != -1 {
endpointUrl = endpointUrl[slashIndex:]
} else {
endpointUrl = "/"
}
}
if !strings.HasPrefix(endpointUrl, f.matchedRule.PathRewritePrefix) {
// The endpoint URL does not match the path rewrite prefix. We are unable to rewrite it back.
api.LogWarnf("The endpoint URL %s does not match the path rewrite prefix %s", endpointUrl, f.matchedRule.PathRewritePrefix)
return false, ""
}
suffix := endpointUrl[len(f.matchedRule.PathRewritePrefix):]
if len(suffix) == 0 {
endpointUrl = f.matchedRule.MatchRulePath
} else {
matchPathHasTrailingSlash := strings.HasSuffix(f.matchedRule.MatchRulePath, "/")
suffixHasLeadingSlash := strings.HasPrefix(suffix, "/")
if matchPathHasTrailingSlash != suffixHasLeadingSlash {
// One has, the other doesn't have.
endpointUrl = f.matchedRule.MatchRulePath + suffix
} else if matchPathHasTrailingSlash {
// Both have.
endpointUrl = f.matchedRule.MatchRulePath + suffix[1:]
} else {
// Neither have.
endpointUrl = f.matchedRule.MatchRulePath + "/" + suffix
}
}
return true, endpointUrl
}
func (f *filter) findSSELineBreak(bufferData string) (error, string) {
// See https://html.spec.whatwg.org/multipage/server-sent-events.html
crIndex := strings.IndexAny(bufferData, "\r")
lfIndex := strings.IndexAny(bufferData, "\n")
if crIndex == -1 && lfIndex == -1 {
// No line break found.
return nil, ""
}
lineBreak := ""
if crIndex != -1 && lfIndex != -1 {
if crIndex+1 != lfIndex {
// Found both line breaks, but they are not adjacent. Skip body processing.
return errors.New("found non-adjacent CR and LF"), ""
}
lineBreak = "\r\n"
} else if crIndex != -1 {
lineBreak = "\r"
} else {
lineBreak = "\n"
}
return nil, lineBreak
}
func (f *filter) findEndpointUrl(bufferData, lineBreak string) (error, string) {
eventIndex := strings.Index(bufferData, "event:")
if eventIndex == -1 {
return nil, ""
}
bufferData = bufferData[eventIndex:]
eventEndIndex := strings.Index(bufferData, lineBreak)
if eventEndIndex == -1 {
return nil, ""
}
eventName := strings.TrimSpace(bufferData[len("event:"):eventEndIndex])
if eventName != "endpoint" {
return fmt.Errorf("the initial event [%s] is not an endpoint event. Skip processing", eventName), ""
}
bufferData = bufferData[eventEndIndex+len(lineBreak):]
dataEndIndex := strings.Index(bufferData, lineBreak)
if dataEndIndex == -1 {
// Data received not enough.
return nil, ""
}
eventData := bufferData[:dataEndIndex]
if !strings.HasPrefix(eventData, "data:") {
return fmt.Errorf("an unexpected non-data field found in the event. Skip processing. Field: %s", eventData), ""
}
return nil, strings.TrimSpace(eventData[len("data:"):])
}
// OnDestroy stops the goroutine
func (f *filter) OnDestroy(reason api.DestroyReason) {
api.LogDebugf("OnDestroy: reason=%v", reason)

View File

@@ -8,7 +8,7 @@
| `modelKey` | string | 选填 | model | 请求body中model参数的位置 |
| `addProviderHeader` | string | 选填 | - | 从model参数中解析出的provider名字放到哪个请求header中 |
| `modelToHeader` | string | 选填 | - | 直接将model参数放到哪个请求header中 |
| `enableOnPathSuffix` | array of string | 选填 | ["/v1/chat/completions"] | 只对这些特定路径后缀的请求生效,可以配置为 "*" 以匹配所有路径 |
| `enableOnPathSuffix` | array of string | 选填 | ["/completions","/embeddings","/images/generations","/audio/speech","/fine_tuning/jobs","/moderations"] | 只对这些特定路径后缀的请求生效,可以配置为 "*" 以匹配所有路径 |
## 运行属性

View File

@@ -358,6 +358,9 @@ func getApiName(path string) provider.ApiName {
if strings.HasSuffix(path, "/v1/files") {
return provider.ApiNameFiles
}
if strings.HasSuffix(path, "/v1/models") {
return provider.ApiNameModels
}
// cohere style
if strings.HasSuffix(path, "/v1/rerank") {
return provider.ApiNameCohereV1Rerank

View File

@@ -36,7 +36,10 @@ func (g *geminiProviderInitializer) ValidateConfig(config *ProviderConfig) error
}
func (g *geminiProviderInitializer) DefaultCapabilities() map[string]string {
return map[string]string{}
return map[string]string{
string(ApiNameChatCompletion): "",
string(ApiNameEmbeddings): "",
}
}
func (g *geminiProviderInitializer) CreateProvider(config ProviderConfig) (Provider, error) {

View File

@@ -20,6 +20,7 @@ import (
const (
moonshotDomain = "api.moonshot.cn"
moonshotChatCompletionPath = "/v1/chat/completions"
moonshotModelsPath = "/v1/models"
)
type moonshotProviderInitializer struct {
@@ -38,6 +39,7 @@ func (m *moonshotProviderInitializer) ValidateConfig(config *ProviderConfig) err
func (m *moonshotProviderInitializer) DefaultCapabilities() map[string]string {
return map[string]string{
string(ApiNameChatCompletion): moonshotChatCompletionPath,
string(ApiNameModels): moonshotModelsPath,
}
}

View File

@@ -30,6 +30,7 @@ func (m *ollamaProviderInitializer) DefaultCapabilities() map[string]string {
// ollama的chat接口path和OpenAI的chat接口一样
string(ApiNameChatCompletion): PathOpenAIChatCompletions,
string(ApiNameEmbeddings): PathOpenAIEmbeddings,
string(ApiNameModels): PathOpenAIModels,
}
}

View File

@@ -21,6 +21,7 @@ const (
defaultOpenaiEmbeddingsPath = "/v1/embeddings"
defaultOpenaiAudioSpeech = "/v1/audio/speech"
defaultOpenaiImageGeneration = "/v1/images/generations"
defaultOpenaiModels = "/v1/models"
)
type openaiProviderInitializer struct {
@@ -37,6 +38,7 @@ func (m *openaiProviderInitializer) DefaultCapabilities() map[string]string {
string(ApiNameEmbeddings): defaultOpenaiEmbeddingsPath,
string(ApiNameImageGeneration): defaultOpenaiImageGeneration,
string(ApiNameAudioSpeech): defaultOpenaiAudioSpeech,
string(ApiNameModels): defaultOpenaiModels,
}
}

View File

@@ -31,12 +31,14 @@ const (
ApiNameAudioSpeech ApiName = "openai/v1/audiospeech"
ApiNameFiles ApiName = "openai/v1/files"
ApiNameBatches ApiName = "openai/v1/batches"
ApiNameModels ApiName = "openai/v1/models"
PathOpenAICompletions = "/v1/completions"
PathOpenAIChatCompletions = "/v1/chat/completions"
PathOpenAIEmbeddings = "/v1/embeddings"
PathOpenAIFiles = "/v1/files"
PathOpenAIBatches = "/v1/batches"
PathOpenAIModels = "/v1/models"
// TODO: 以下是一些非标准的API名称需要进一步确认是否支持
ApiNameCohereV1Rerank ApiName = "cohere/v1/rerank"

View File

@@ -27,6 +27,7 @@ func main() {
}
const (
defaultMaxBodyBytes uint32 = 100 * 1024 * 1024
// Context consts
StatisticsRequestStartTime = "ai-statistics-request-start-time"
StatisticsFirstTokenTime = "ai-statistics-first-token-time"
@@ -176,6 +177,11 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, config AIStatisticsConfig, lo
if consumer, _ := proxywasm.GetHttpRequestHeader(ConsumerKey); consumer != "" {
ctx.SetContext(ConsumerKey, consumer)
}
hasRequestBody := wrapper.HasRequestBody()
if hasRequestBody {
_ = proxywasm.RemoveHttpRequestHeader("Content-Length")
ctx.SetRequestBodyBufferLimit(defaultMaxBodyBytes)
}
// Set user defined log & span attributes which type is fixed_value
setAttributeBySource(ctx, config, FixedValue, nil, log)

View File

@@ -8,7 +8,7 @@ replace amap-tools => ../amap-tools
require (
amap-tools v0.0.0-00010101000000-000000000000
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250423015849-23258157a406
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250507130917-ed12a186173a
quark-search v0.0.0-00010101000000-000000000000
)

View File

@@ -6,8 +6,8 @@ github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+
github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250423015849-23258157a406 h1:pWZsjfarQyUPlzJ9CMy4C5iHl0jb2jntscd1wCGwGB0=
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250423015849-23258157a406/go.mod h1:yObZXF1xTx/8peEsSbtHIzz7KlTr/tZCrokIHtwF0Jk=
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250507130917-ed12a186173a h1:CvTkMBU9+SGIyJEJYFEvg/esoVbLzQP9WVeoZzMHM9E=
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250507130917-ed12a186173a/go.mod h1:yObZXF1xTx/8peEsSbtHIzz7KlTr/tZCrokIHtwF0Jk=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=

View File

@@ -2,7 +2,10 @@ module amap-tools
go 1.24.1
require github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250407124215-3431eeb8d374
require (
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250507122328-b62384cff88a
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250402062734-d50d98c305f0
)
require (
dario.cat/mergo v1.0.1 // indirect
@@ -12,8 +15,7 @@ require (
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/higress-group/gjson_template v0.0.0-20250331062947-760bb2f96985 // indirect
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250402062734-d50d98c305f0 // indirect
github.com/higress-group/gjson_template v0.0.0-20250413075336-4c4161ed428b // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/invopop/jsonschema v0.13.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect

View File

@@ -6,8 +6,8 @@ github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+
github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250407124215-3431eeb8d374 h1:Ht+XEuYcuytDa6YkgTXR/94h+/XAafX0GhGXcnr9siw=
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250407124215-3431eeb8d374/go.mod h1:nAmuA22tHQhn8to3y980Ut7FFv/Ayjj/B7n/F8Wf5JY=
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250507122328-b62384cff88a h1:VQrtP0CR4pgIL3FGnIAb+uY3yRwaMQk2c3AT3p+LVwk=
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250507122328-b62384cff88a/go.mod h1:yObZXF1xTx/8peEsSbtHIzz7KlTr/tZCrokIHtwF0Jk=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
@@ -20,8 +20,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/higress-group/gjson_template v0.0.0-20250331062947-760bb2f96985 h1:rOxn1GyVZGphQ1GeE1bxSCtRNxtNLzE9KpA5Zyq5Ui0=
github.com/higress-group/gjson_template v0.0.0-20250331062947-760bb2f96985/go.mod h1:rU3M+Tq5VrQOo0dxpKHGb03Ty0sdWIZfAH+YCOACx/Y=
github.com/higress-group/gjson_template v0.0.0-20250413075336-4c4161ed428b h1:rRI9+ThQbe+nw4jUiYEyOFaREkXCMMW9k1X2gy2d6pE=
github.com/higress-group/gjson_template v0.0.0-20250413075336-4c4161ed428b/go.mod h1:rU3M+Tq5VrQOo0dxpKHGb03Ty0sdWIZfAH+YCOACx/Y=
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250402062734-d50d98c305f0 h1:Ta+RBsZYML3hjoenbGJoS2L6aWJN+hqlxKoqzj/Y2SY=
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250402062734-d50d98c305f0/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=

View File

@@ -58,11 +58,11 @@ func (t AroundSearchRequest) Call(ctx server.HttpContext, s server.Server) error
url := fmt.Sprintf("http://restapi.amap.com/v3/place/around?key=%s&location=%s&radius=%s&keywords=%s&source=ts_mcp", serverConfig.ApiKey, url.QueryEscape(t.Location), url.QueryEscape(t.Radius), url.QueryEscape(t.Keywords))
return ctx.RouteCall(http.MethodGet, url,
[][2]string{{"Accept", "application/json"}}, nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) {
[][2]string{{"Accept", "application/json"}}, nil, func(sendDirectly bool, statusCode int, responseHeaders [][2]string, responseBody []byte) {
if statusCode != http.StatusOK {
utils.OnMCPToolCallError(ctx, fmt.Errorf("around search call failed, status: %d", statusCode))
utils.OnMCPToolCallError(sendDirectly, ctx, fmt.Errorf("around search call failed, status: %d", statusCode))
return
}
utils.SendMCPToolTextResult(ctx, string(responseBody))
utils.SendMCPToolTextResult(sendDirectly, ctx, string(responseBody))
})
}

View File

@@ -57,11 +57,11 @@ func (t BicyclingRequest) Call(ctx server.HttpContext, s server.Server) error {
url := fmt.Sprintf("http://restapi.amap.com/v4/direction/bicycling?key=%s&origin=%s&destination=%s&source=ts_mcp", serverConfig.ApiKey, url.QueryEscape(t.Origin), url.QueryEscape(t.Destination))
return ctx.RouteCall(http.MethodGet, url,
[][2]string{{"Accept", "application/json"}}, nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) {
[][2]string{{"Accept", "application/json"}}, nil, func(sendDirectly bool, statusCode int, responseHeaders [][2]string, responseBody []byte) {
if statusCode != http.StatusOK {
utils.OnMCPToolCallError(ctx, fmt.Errorf("bicycling call failed, status: %d", statusCode))
utils.OnMCPToolCallError(sendDirectly, ctx, fmt.Errorf("bicycling call failed, status: %d", statusCode))
return
}
utils.SendMCPToolTextResult(ctx, string(responseBody))
utils.SendMCPToolTextResult(sendDirectly, ctx, string(responseBody))
})
}

View File

@@ -57,11 +57,11 @@ func (t DrivingRequest) Call(ctx server.HttpContext, s server.Server) error {
url := fmt.Sprintf("http://restapi.amap.com/v3/direction/driving?key=%s&origin=%s&destination=%s&source=ts_mcp", serverConfig.ApiKey, url.QueryEscape(t.Origin), url.QueryEscape(t.Destination))
return ctx.RouteCall(http.MethodGet, url,
[][2]string{{"Accept", "application/json"}}, nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) {
[][2]string{{"Accept", "application/json"}}, nil, func(sendDirectly bool, statusCode int, responseHeaders [][2]string, responseBody []byte) {
if statusCode != http.StatusOK {
utils.OnMCPToolCallError(ctx, fmt.Errorf("driving call failed, status: %d", statusCode))
utils.OnMCPToolCallError(sendDirectly, ctx, fmt.Errorf("driving call failed, status: %d", statusCode))
return
}
utils.SendMCPToolTextResult(ctx, string(responseBody))
utils.SendMCPToolTextResult(sendDirectly, ctx, string(responseBody))
})
}

View File

@@ -59,11 +59,11 @@ func (t TransitIntegratedRequest) Call(ctx server.HttpContext, s server.Server)
url := fmt.Sprintf("http://restapi.amap.com/v3/direction/transit/integrated?key=%s&origin=%s&destination=%s&city=%s&cityd=%s&source=ts_mcp", serverConfig.ApiKey, url.QueryEscape(t.Origin), url.QueryEscape(t.Destination), url.QueryEscape(t.City), url.QueryEscape(t.Cityd))
return ctx.RouteCall(http.MethodGet, url,
[][2]string{{"Accept", "application/json"}}, nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) {
[][2]string{{"Accept", "application/json"}}, nil, func(sendDirectly bool, statusCode int, responseHeaders [][2]string, responseBody []byte) {
if statusCode != http.StatusOK {
utils.OnMCPToolCallError(ctx, fmt.Errorf("transit integrated call failed, status: %d", statusCode))
utils.OnMCPToolCallError(sendDirectly, ctx, fmt.Errorf("transit integrated call failed, status: %d", statusCode))
return
}
utils.SendMCPToolTextResult(ctx, string(responseBody))
utils.SendMCPToolTextResult(sendDirectly, ctx, string(responseBody))
})
}

View File

@@ -57,11 +57,11 @@ func (t WalkingRequest) Call(ctx server.HttpContext, s server.Server) error {
url := fmt.Sprintf("http://restapi.amap.com/v3/direction/walking?key=%s&origin=%s&destination=%s&source=ts_mcp", serverConfig.ApiKey, url.QueryEscape(t.Origin), url.QueryEscape(t.Destination))
return ctx.RouteCall(http.MethodGet, url,
[][2]string{{"Accept", "application/json"}}, nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) {
[][2]string{{"Accept", "application/json"}}, nil, func(sendDirectly bool, statusCode int, responseHeaders [][2]string, responseBody []byte) {
if statusCode != http.StatusOK {
utils.OnMCPToolCallError(ctx, fmt.Errorf("walking call failed, status: %d", statusCode))
utils.OnMCPToolCallError(sendDirectly, ctx, fmt.Errorf("walking call failed, status: %d", statusCode))
return
}
utils.SendMCPToolTextResult(ctx, string(responseBody))
utils.SendMCPToolTextResult(sendDirectly, ctx, string(responseBody))
})
}

View File

@@ -57,11 +57,11 @@ func (t DistanceRequest) Call(ctx server.HttpContext, s server.Server) error {
url := fmt.Sprintf("http://restapi.amap.com/v3/distance?key=%s&origins=%s&destination=%s&type=%s&source=ts_mcp", serverConfig.ApiKey, url.QueryEscape(t.Origins), url.QueryEscape(t.Destination), url.QueryEscape(t.Type))
return ctx.RouteCall(http.MethodGet, url,
[][2]string{{"Accept", "application/json"}}, nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) {
[][2]string{{"Accept", "application/json"}}, nil, func(sendDirectly bool, statusCode int, responseHeaders [][2]string, responseBody []byte) {
if statusCode != http.StatusOK {
utils.OnMCPToolCallError(ctx, fmt.Errorf("distance call failed, status: %d", statusCode))
utils.OnMCPToolCallError(sendDirectly, ctx, fmt.Errorf("distance call failed, status: %d", statusCode))
return
}
utils.SendMCPToolTextResult(ctx, string(responseBody))
utils.SendMCPToolTextResult(sendDirectly, ctx, string(responseBody))
})
}

View File

@@ -58,11 +58,11 @@ func (t GeoRequest) Call(ctx server.HttpContext, s server.Server) error {
apiKey := serverConfig.ApiKey
url := fmt.Sprintf("https://restapi.amap.com/v3/geocode/geo?key=%s&address=%s&city=%s&source=ts_mcp", apiKey, url.QueryEscape(t.Address), url.QueryEscape(t.City))
return ctx.RouteCall(http.MethodGet, url,
[][2]string{{"Accept", "application/json"}}, nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) {
[][2]string{{"Accept", "application/json"}}, nil, func(sendDirectly bool, statusCode int, responseHeaders [][2]string, responseBody []byte) {
if statusCode != http.StatusOK {
utils.OnMCPToolCallError(ctx, fmt.Errorf("geo call failed, status: %d", statusCode))
utils.OnMCPToolCallError(sendDirectly, ctx, fmt.Errorf("geo call failed, status: %d", statusCode))
return
}
utils.SendMCPToolTextResult(ctx, string(responseBody))
utils.SendMCPToolTextResult(sendDirectly, ctx, string(responseBody))
})
}

View File

@@ -70,12 +70,12 @@ func (t IPLocationRequest) Call(ctx server.HttpContext, s server.Server) error {
}
url := fmt.Sprintf("https://restapi.amap.com/v3/ip?ip=%s&key=%s&source=ts_mcp", url.QueryEscape(t.IP), serverConfig.ApiKey)
return ctx.RouteCall(http.MethodGet, url,
[][2]string{{"Accept", "application/json"}}, nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) {
[][2]string{{"Accept", "application/json"}}, nil, func(sendDirectly bool, statusCode int, responseHeaders [][2]string, responseBody []byte) {
if statusCode != http.StatusOK {
utils.OnMCPToolCallError(ctx, fmt.Errorf("ip location call failed, status: %d", statusCode))
utils.OnMCPToolCallError(sendDirectly, ctx, fmt.Errorf("ip location call failed, status: %d", statusCode))
return
}
utils.SendMCPToolTextResult(ctx, string(responseBody))
utils.SendMCPToolTextResult(sendDirectly, ctx, string(responseBody))
})
}

View File

@@ -56,11 +56,11 @@ func (t ReGeocodeRequest) Call(ctx server.HttpContext, s server.Server) error {
url := fmt.Sprintf("http://restapi.amap.com/v3/geocode/regeo?location=%s&key=%s&source=ts_mcp", url.QueryEscape(t.Location), serverConfig.ApiKey)
return ctx.RouteCall(http.MethodGet, url,
[][2]string{{"Accept", "application/json"}}, nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) {
[][2]string{{"Accept", "application/json"}}, nil, func(sendDirectly bool, statusCode int, responseHeaders [][2]string, responseBody []byte) {
if statusCode != http.StatusOK {
utils.OnMCPToolCallError(ctx, fmt.Errorf("regeocode call failed, status: %d", statusCode))
utils.OnMCPToolCallError(sendDirectly, ctx, fmt.Errorf("regeocode call failed, status: %d", statusCode))
return
}
utils.SendMCPToolTextResult(ctx, string(responseBody))
utils.SendMCPToolTextResult(sendDirectly, ctx, string(responseBody))
})
}

View File

@@ -56,11 +56,11 @@ func (t SearchDetailRequest) Call(ctx server.HttpContext, s server.Server) error
url := fmt.Sprintf("http://restapi.amap.com/v3/place/detail?id=%s&key=%s&source=ts_mcp", url.QueryEscape(t.ID), serverConfig.ApiKey)
return ctx.RouteCall(http.MethodGet, url,
[][2]string{{"Accept", "application/json"}}, nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) {
[][2]string{{"Accept", "application/json"}}, nil, func(sendDirectly bool, statusCode int, responseHeaders [][2]string, responseBody []byte) {
if statusCode != http.StatusOK {
utils.OnMCPToolCallError(ctx, fmt.Errorf("search detail call failed, status: %d", statusCode))
utils.OnMCPToolCallError(sendDirectly, ctx, fmt.Errorf("search detail call failed, status: %d", statusCode))
return
}
utils.SendMCPToolTextResult(ctx, string(responseBody))
utils.SendMCPToolTextResult(sendDirectly, ctx, string(responseBody))
})
}

View File

@@ -58,11 +58,11 @@ func (t TextSearchRequest) Call(ctx server.HttpContext, s server.Server) error {
url := fmt.Sprintf("http://restapi.amap.com/v3/place/text?key=%s&keywords=%s&city=%s&citylimit=%s&source=ts_mcp", serverConfig.ApiKey, url.QueryEscape(t.Keywords), url.QueryEscape(t.City), url.QueryEscape(t.Citylimit))
return ctx.RouteCall(http.MethodGet, url,
[][2]string{{"Accept", "application/json"}}, nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) {
[][2]string{{"Accept", "application/json"}}, nil, func(sendDirectly bool, statusCode int, responseHeaders [][2]string, responseBody []byte) {
if statusCode != http.StatusOK {
utils.OnMCPToolCallError(ctx, fmt.Errorf("text search call failed, status: %d", statusCode))
utils.OnMCPToolCallError(sendDirectly, ctx, fmt.Errorf("text search call failed, status: %d", statusCode))
return
}
utils.SendMCPToolTextResult(ctx, string(responseBody))
utils.SendMCPToolTextResult(sendDirectly, ctx, string(responseBody))
})
}

View File

@@ -56,11 +56,11 @@ func (t WeatherRequest) Call(ctx server.HttpContext, s server.Server) error {
url := fmt.Sprintf("http://restapi.amap.com/v3/weather/weatherInfo?city=%s&key=%s&source=ts_mcp&extensions=all", url.QueryEscape(t.City), serverConfig.ApiKey)
return ctx.RouteCall(http.MethodGet, url,
[][2]string{{"Accept", "application/json"}}, nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) {
[][2]string{{"Accept", "application/json"}}, nil, func(sendDirectly bool, statusCode int, responseHeaders [][2]string, responseBody []byte) {
if statusCode != http.StatusOK {
utils.OnMCPToolCallError(ctx, fmt.Errorf("weather call failed, status: %d", statusCode))
utils.OnMCPToolCallError(sendDirectly, ctx, fmt.Errorf("weather call failed, status: %d", statusCode))
return
}
utils.SendMCPToolTextResult(ctx, string(responseBody))
utils.SendMCPToolTextResult(sendDirectly, ctx, string(responseBody))
})
}

View File

@@ -3,7 +3,7 @@ module quark-search
go 1.24.1
require (
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250407124215-3431eeb8d374
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250507122328-b62384cff88a
github.com/tidwall/gjson v1.18.0
)
@@ -15,7 +15,7 @@ require (
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/higress-group/gjson_template v0.0.0-20250331062947-760bb2f96985 // indirect
github.com/higress-group/gjson_template v0.0.0-20250413075336-4c4161ed428b // indirect
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250402062734-d50d98c305f0 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/invopop/jsonschema v0.13.0 // indirect

View File

@@ -6,8 +6,8 @@ github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+
github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250407124215-3431eeb8d374 h1:Ht+XEuYcuytDa6YkgTXR/94h+/XAafX0GhGXcnr9siw=
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250407124215-3431eeb8d374/go.mod h1:nAmuA22tHQhn8to3y980Ut7FFv/Ayjj/B7n/F8Wf5JY=
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250507122328-b62384cff88a h1:VQrtP0CR4pgIL3FGnIAb+uY3yRwaMQk2c3AT3p+LVwk=
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250507122328-b62384cff88a/go.mod h1:yObZXF1xTx/8peEsSbtHIzz7KlTr/tZCrokIHtwF0Jk=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
@@ -20,8 +20,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/higress-group/gjson_template v0.0.0-20250331062947-760bb2f96985 h1:rOxn1GyVZGphQ1GeE1bxSCtRNxtNLzE9KpA5Zyq5Ui0=
github.com/higress-group/gjson_template v0.0.0-20250331062947-760bb2f96985/go.mod h1:rU3M+Tq5VrQOo0dxpKHGb03Ty0sdWIZfAH+YCOACx/Y=
github.com/higress-group/gjson_template v0.0.0-20250413075336-4c4161ed428b h1:rRI9+ThQbe+nw4jUiYEyOFaREkXCMMW9k1X2gy2d6pE=
github.com/higress-group/gjson_template v0.0.0-20250413075336-4c4161ed428b/go.mod h1:rU3M+Tq5VrQOo0dxpKHGb03Ty0sdWIZfAH+YCOACx/Y=
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250402062734-d50d98c305f0 h1:Ta+RBsZYML3hjoenbGJoS2L6aWJN+hqlxKoqzj/Y2SY=
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250402062734-d50d98c305f0/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=

View File

@@ -99,9 +99,9 @@ func (t WebSearch) Call(ctx server.HttpContext, s server.Server) error {
}
return ctx.RouteCall(http.MethodGet, fmt.Sprintf("https://cloud-iqs.aliyuncs.com/search/genericSearch?query=%s", url.QueryEscape(t.Query)),
[][2]string{{"Accept", "application/json"},
{"X-API-Key", serverConfig.ApiKey}}, nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) {
{"X-API-Key", serverConfig.ApiKey}}, nil, func(sendDirectly bool, statusCode int, responseHeaders [][2]string, responseBody []byte) {
if statusCode != http.StatusOK {
utils.OnMCPToolCallError(ctx, fmt.Errorf("quark search call failed, status: %d", statusCode))
utils.OnMCPToolCallError(sendDirectly, ctx, fmt.Errorf("quark search call failed, status: %d", statusCode))
return
}
jsonObj := gjson.ParseBytes(responseBody)
@@ -125,6 +125,6 @@ func (t WebSearch) Call(ctx server.HttpContext, s server.Server) error {
results = append(results, result.Format())
}
}
utils.SendMCPToolTextResult(ctx, fmt.Sprintf("# Search Results\n\n%s", strings.Join(results, "\n\n")))
utils.SendMCPToolTextResult(sendDirectly, ctx, fmt.Sprintf("# Search Results\n\n%s", strings.Join(results, "\n\n")))
})
}

View File

@@ -84,6 +84,7 @@ type watcher struct {
nacosClientConfig *constant.ClientConfig
namespace string
clusterId string
authOption provider.AuthOption
}
type WatcherOption func(w *watcher)
@@ -131,6 +132,8 @@ func NewWatcher(cache memory.Cache, opts ...WatcherOption) (provider.Watcher, er
constant.WithNamespaceId(w.NacosNamespaceId),
constant.WithAccessKey(w.NacosAccessKey),
constant.WithSecretKey(w.NacosSecretKey),
constant.WithUsername(w.authOption.NacosUsername),
constant.WithPassword(w.authOption.NacosPassword),
)
initTimer := time.NewTimer(DefaultInitTimeout)
@@ -241,6 +244,12 @@ func WithClusterId(id string) WatcherOption {
}
}
func WithAuthOption(authOption provider.AuthOption) WatcherOption {
return func(w *watcher) {
w.authOption = authOption
}
}
func (w *watcher) Run() {
ticker := time.NewTicker(time.Duration(w.NacosRefreshInterval))
defer ticker.Stop()
@@ -633,6 +642,8 @@ func (w *watcher) buildServiceEntryForMcpServer(mcpServer *provider.McpServer, c
constant.WithNamespaceId(serviceNamespace),
constant.WithAccessKey(w.NacosAccessKey),
constant.WithSecretKey(w.NacosSecretKey),
constant.WithUsername(w.authOption.NacosUsername),
constant.WithPassword(w.authOption.NacosPassword),
)
client, err := clients.NewNamingClient(vo.NacosClientParam{
ClientConfig: namingConfig,
@@ -678,7 +689,7 @@ func (w *watcher) getServiceCallback(server *provider.McpServer, configGroup, da
Meta: config.Meta{
GroupVersionKind: gvk.ServiceEntry,
Name: fmt.Sprintf("%s-%s-%s", provider.IstioMcpAutoGeneratedSeName, configGroup, strings.TrimSuffix(dataId, ".json")),
Namespace: w.namespace,
Namespace: "mcp",
},
Spec: serviceEntry,
}
@@ -695,12 +706,12 @@ func (w *watcher) getServiceCallback(server *provider.McpServer, configGroup, da
w.cache.UpdateConfigCache(gvk.DestinationRule, configKey, dr, false)
}
w.cache.UpdateConfigCache(gvk.ServiceEntry, configKey, se, false)
vs := w.buildVirtualServiceForMcpServer(serviceEntry, configGroup, dataId, path, server.Name)
vs := w.buildVirtualServiceForMcpServer(serviceEntry, configGroup, dataId, path, server)
w.cache.UpdateConfigCache(gvk.VirtualService, configKey, vs, false)
}
}
func (w *watcher) buildVirtualServiceForMcpServer(serviceentry *v1alpha3.ServiceEntry, group, dataId, path, serverName string) *config.Config {
func (w *watcher) buildVirtualServiceForMcpServer(serviceentry *v1alpha3.ServiceEntry, group, dataId, path string, server *provider.McpServer) *config.Config {
if serviceentry == nil {
return nil
}
@@ -717,7 +728,7 @@ func (w *watcher) buildVirtualServiceForMcpServer(serviceentry *v1alpha3.Service
common2.CreateConvertedName(constants.IstioIngressGatewayName, cleanHost))
}
routeName := fmt.Sprintf("%s-%s-%s", provider.IstioMcpAutoGeneratedHttpRouteName, group, strings.TrimSuffix(dataId, ".json"))
mergePath := "/" + serverName
mergePath := "/" + server.Name
if w.McpServerBaseUrl != "/" {
mergePath = strings.TrimSuffix(w.McpServerBaseUrl, "/") + mergePath
}
@@ -751,6 +762,12 @@ func (w *watcher) buildVirtualServiceForMcpServer(serviceentry *v1alpha3.Service
}},
}
if server.Protocol == provider.McpStreambleProtocol {
vs.Http[0].Rewrite = &v1alpha3.HTTPRewrite{
Uri: path,
}
}
mcpServerLog.Debugf("construct virtualservice %v", vs)
return &config.Config{

View File

@@ -203,6 +203,7 @@ func (r *Reconciler) generateWatcherFromRegistryConfig(registry *apiv1.RegistryC
mcpserver.WithEnableMcpServer(registry.EnableMCPServer),
mcpserver.WithClusterId(r.clusterId),
mcpserver.WithNamespace(r.namespace),
mcpserver.WithAuthOption(authOption),
)
} else {
watcher, err = nacosv2.NewWatcher(