Compare commits

...

42 Commits

Author SHA1 Message Date
johnlanni
48b220453b release 2.1.2 2025-04-29 20:53:50 +08:00
mirror
489a800868 add: add mcp-context7 descriptions (#2149) 2025-04-29 20:44:00 +08:00
澄潭
60c9f21e1c When the service source type is nacos3, if mcpserver is turned off, then the discovery mechanism of nacos2 will be enabled (#2150) 2025-04-29 17:29:52 +08:00
Jingze
ab73f21017 fix: make mcp server redis client config based (#2145)
Co-authored-by: daijingze_mac <18373118@buaa.edu.cn>
2025-04-29 14:27:48 +08:00
EricaLiu
806563298b fix : when nacos push empty service instance list, should skip generate (#2144) 2025-04-29 11:38:51 +08:00
github-actions[bot]
02fabbb35f Update helm translated README.zh.md (#2141) 2025-04-29 09:28:20 +08:00
johnlanni
07154d1f49 set mcp-go dependency to 0.12 2025-04-28 23:02:28 +08:00
johnlanni
db30c0962a update go mod 2025-04-28 22:04:21 +08:00
johnlanni
731fe43d14 update envoy-release version 2025-04-28 21:59:30 +08:00
EricaLiu
5bd20aa559 feat : support mcp server auto discovery for nacos registry (#2122)
Co-authored-by: 澄潭 <zty98751@alibaba-inc.com>
2025-04-28 21:58:17 +08:00
johnlanni
a2e4f944e9 rel 2.1.2-rc.1 2025-04-28 20:39:02 +08:00
johnlanni
7955aec639 change golang-filter build image 2025-04-28 19:43:17 +08:00
johnlanni
e12feb9f57 golang-filter use go 1.22 2025-04-28 19:21:09 +08:00
zty98751
03b4144cff update submodule commit 2025-04-28 17:42:58 +08:00
Jingze
c382635e7f fix: Refactor MCP Server into MCP Session and MCP Server (#2120) 2025-04-28 13:42:14 +08:00
007gzs
e381806ba0 fix: ai_data_masking add compatibility handling for non-compliant API response structures (#2130) 2025-04-27 14:31:02 +08:00
johnlanni
52114b37f8 update mcp server config fields in mcp-bridge api 2025-04-27 11:10:08 +08:00
mirror
b4e68c02f9 add mcp yuque descriptions (#2125) 2025-04-25 18:08:42 +08:00
Tsukilc
c241ccf19d test: add test for /pkg/ingress/kube/common (#2123) 2025-04-24 20:03:57 +08:00
澄潭
e4fa1e6390 Update README_ZH.md 2025-04-24 19:08:40 +08:00
澄潭
b103b9d7cb Update README.md 2025-04-24 19:08:10 +08:00
johnlanni
90b02a90e0 update mcpbridge proto 2025-04-24 15:52:17 +08:00
mirror
38f718b965 update github & e2bdev mcp descriptions (#2107) 2025-04-23 20:08:21 +08:00
johnlanni
8752a763c2 update all-in-one mcp-server 2025-04-23 14:42:44 +08:00
HaoJie Liu
a57173ce28 feat(ai-proxy): support Amazon Bedrock (#2039) 2025-04-22 22:36:14 +08:00
mirror
3a8d8f5b94 update mcp descriptions (#2105) 2025-04-22 17:01:41 +08:00
Kent Dong
1c37c361e1 feat: Support extracting model argument from body in multipart/form-data format (#1940) 2025-04-22 13:52:50 +08:00
Se7en
b8133a95b2 feat: optimize elasticsearch ai-search plugin and update related docs" (#2100) 2025-04-22 13:33:38 +08:00
johnlanni
36d5d391b8 update README.md 2025-04-21 09:59:37 +08:00
johnlanni
1da9a07866 update README 2025-04-21 09:56:23 +08:00
ZeruiYang
8620838f8b fix: update module replacements (#2090) 2025-04-19 18:13:42 +08:00
waTErMo0n
e7d2005382 feat:Getting MatchLabels dynamically via gatewaySelectorKey/Value #1857 (#1883) 2025-04-18 17:46:47 +08:00
johnlanni
4f47d3fc12 rel: Release 2.1.1 2025-04-18 16:47:07 +08:00
rinfx
6773482300 Enhance the compatibility of AI observability plugins with different LLM suppliers (#2088) 2025-04-18 16:19:59 +08:00
johnlanni
b6d61f9568 update README 2025-04-18 13:43:33 +08:00
Jingze
1834d4acef fix: support mcp server database reconnect and fix tool/list method denied (#2074) 2025-04-18 11:19:56 +08:00
johnlanni
7f9ae38e51 update all-in-one mcp-server depenednecy 2025-04-17 16:25:00 +08:00
mirror
b13bce6a36 add mcp descriptions (#2080) 2025-04-17 13:46:31 +08:00
liseri
275cac9dbb fix wasm-go/jwt-auth claims_to_headers bug (#2057)
Co-authored-by: Kent Dong <ch3cho@qq.com>
2025-04-16 20:25:01 +08:00
澄潭
8cce7f5d50 add mcp servers (#2076) 2025-04-16 14:48:53 +08:00
rinfx
4f0834d817 rm plugin id after use (#2070) 2025-04-15 19:47:43 +08:00
Kent Dong
7cf0dae824 feat: Support building waf plugin using Makefile (#2061) 2025-04-15 10:25:59 +08:00
265 changed files with 44722 additions and 956 deletions

View File

@@ -144,7 +144,7 @@ docker-buildx-push: clean-env docker.higress-buildx
export PARENT_GIT_TAG:=$(shell cat VERSION)
export PARENT_GIT_REVISION:=$(TAG)
export ENVOY_PACKAGE_URL_PATTERN?=https://github.com/higress-group/proxy/releases/download/v2.1.4/envoy-symbol-ARCH.tar.gz
export ENVOY_PACKAGE_URL_PATTERN?=https://github.com/higress-group/proxy/releases/download/v2.1.5/envoy-symbol-ARCH.tar.gz
build-envoy: prebuild
./tools/hack/build-envoy.sh

View File

@@ -11,7 +11,8 @@
[![Build Status](https://github.com/alibaba/higress/actions/workflows/build-and-test.yaml/badge.svg?branch=main)](https://github.com/alibaba/higress/actions)
[![license](https://img.shields.io/github/license/alibaba/higress.svg)](https://www.apache.org/licenses/LICENSE-2.0.html)
<a href="https://trendshift.io/repositories/10918" target="_blank"><img src="https://trendshift.io/api/badge/repositories/10918" alt="alibaba%2Fhigress | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://trendshift.io/repositories/10918" target="_blank"><img src="https://trendshift.io/api/badge/repositories/10918" alt="alibaba%2Fhigress | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a> <a href="https://www.producthunt.com/posts/higress?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-higress" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=951287&theme=light&t=1745492822283" alt="Higress - Global&#0032;APIs&#0032;as&#0032;MCP&#0032;powered&#0032;by&#0032;AI&#0032;Gateway | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
</div>
[**Official Site**](https://higress.ai/en/) &nbsp; |
@@ -22,13 +23,21 @@
English | <a href="README_ZH.md">中文<a/> | <a href="README_JP.md">日本語<a/>
</p>
## What is Higress?
Higress is a cloud-native API gateway based on Istio and Envoy, which can be extended with Wasm plugins written in Go/Rust/JS. It provides dozens of ready-to-use general-purpose plugins and an out-of-the-box console (try the [demo here](http://demo.higress.io/)).
Higress was born within Alibaba to solve the issues of Tengine reload affecting long-connection services and insufficient load balancing capabilities for gRPC/Dubbo.
### Core Use Cases
Alibaba Cloud has built its cloud-native API gateway product based on Higress, providing 99.99% gateway high availability guarantee service capabilities for a large number of enterprise customers.
Higress's AI gateway capabilities support all [mainstream model providers](https://github.com/alibaba/higress/tree/main/plugins/wasm-go/extensions/ai-proxy/provider) both domestic and international. It also supports hosting MCP (Model Context Protocol) Servers through its plugin mechanism, enabling AI Agents to easily call various tools and services. With the [openapi-to-mcp tool](https://github.com/higress-group/openapi-to-mcpserver), you can quickly convert OpenAPI specifications into remote MCP servers for hosting. Higress provides unified management for both LLM API and MCP API.
Higress's AI gateway capabilities support all [mainstream model providers](https://github.com/alibaba/higress/tree/main/plugins/wasm-go/extensions/ai-proxy/provider) both domestic and international, as well as self-built DeepSeek models based on vllm/ollama. Within Alibaba Cloud, it supports AI businesses such as Tongyi Qianwen APP, Bailian large model API, and machine learning PAI platform. It also serves leading AIGC enterprises (such as Zero One Infinite) and AI products (such as FastGPT).
**🌟 Try it now at [https://mcp.higress.ai/](https://mcp.higress.ai/)** to experience Higress-hosted Remote MCP Servers firsthand:
![Higress MCP Server Platform](https://img.alicdn.com/imgextra/i2/O1CN01nmVa0a1aChgpyyWOX_!!6000000003294-0-tps-3430-1742.jpg)
### Enterprise Adoption
Higress was born within Alibaba to solve the issues of Tengine reload affecting long-connection services and insufficient load balancing capabilities for gRPC/Dubbo. Within Alibaba Cloud, Higress's AI gateway capabilities support core AI applications such as Tongyi Bailian model studio, machine learning PAI platform, and other critical AI services. Alibaba Cloud has built its cloud-native API gateway product based on Higress, providing 99.99% gateway high availability guarantee service capabilities for a large number of enterprise customers.
## Summary
@@ -64,28 +73,28 @@ For other installation methods such as Helm deployment under K8s, please refer t
## Use Cases
- **AI Gateway**:
Higress can connect to all LLM model providers both domestic and international using a unified protocol, while also providing rich AI observability, multi-model load balancing/fallback, AI token rate limiting, AI caching, and other capabilities:
![](https://img.alicdn.com/imgextra/i2/O1CN01izmBNX1jbHT7lP3Yr_!!6000000004566-0-tps-1920-1080.jpg)
- **MCP Server Hosting**:
Higress, as an Envoy-based API gateway, supports hosting MCP Servers through its plugin mechanism. MCP (Model Context Protocol) is essentially an AI-friendly API that enables AI Agents to more easily call various tools and services. Higress provides unified capabilities for authentication, authorization, rate limiting, and observability for tool calls, simplifying the development and deployment of AI applications.
Higress hosts MCP Servers through its plugin mechanism, enabling AI Agents to easily call various tools and services. With the [openapi-to-mcp tool](https://github.com/higress-group/openapi-to-mcpserver), you can quickly convert OpenAPI specifications into remote MCP servers.
![](https://img.alicdn.com/imgextra/i1/O1CN01wv8H4g1mS4MUzC1QC_!!6000000004952-2-tps-1764-597.png)
By hosting MCP Servers with Higress, you can achieve:
- Unified authentication and authorization mechanisms, ensuring the security of AI tool calls
- Fine-grained rate limiting to prevent abuse and resource exhaustion
- Comprehensive audit logs recording all tool call behaviors
- Rich observability for monitoring the performance and health of tool calls
- Simplified deployment and management through Higress's plugin mechanism for quickly adding new MCP Servers
- Dynamic updates without disruption: Thanks to Envoy's friendly handling of long connections and Wasm plugin's dynamic update mechanism, MCP Server logic can be updated on-the-fly without any traffic disruption or connection drops
Key benefits of hosting MCP Servers with Higress:
- Unified authentication and authorization mechanisms
- Fine-grained rate limiting to prevent abuse
- Comprehensive audit logs for all tool calls
- Rich observability for monitoring performance
- Simplified deployment through Higress's plugin mechanism
- Dynamic updates without disruption or connection drops
[Learn more...](https://higress.cn/en/ai/mcp-quick-start/?spm=36971b57.7beea2de.0.0.d85f20a94jsWGm)
- **AI Gateway**:
Higress connects to all LLM model providers using a unified protocol, with AI observability, multi-model load balancing, token rate limiting, and caching capabilities:
![](https://img.alicdn.com/imgextra/i2/O1CN01izmBNX1jbHT7lP3Yr_!!6000000004566-0-tps-1920-1080.jpg)
- **Kubernetes ingress controller**:
Higress can function as a feature-rich ingress controller, which is compatible with many annotations of K8s' nginx ingress controller.

View File

@@ -22,15 +22,21 @@
</p>
## Higressとは
Higressは、IstioとEnvoyをベースにしたクラウドネイティブAPIゲートウェイで、Go/Rust/JSなどを使用してWasmプラグインを作成できます。数十の既製の汎用プラグインと、すぐに使用できるコンソールを提供していますデモは[こちら](http://demo.higress.io/))。
Higressは、Tengineのリロードが長時間接続のビジネスに影響を与える問題や、gRPC/Dubboの負荷分散能力の不足を解決するために、Alibaba内部で誕生しました。
### 主な使用シナリオ
Alibaba Cloudは、Higressを基盤にクラウドネイティブAPIゲートウェイ製品を構築し、多くの企業顧客に99.99%のゲートウェイ高可用性保証サービスを提供しています。
HigressのAIゲートウェイ機能は、国内外のすべての[主要モデルプロバイダー](https://github.com/alibaba/higress/tree/main/plugins/wasm-go/extensions/ai-proxy/provider)をサポートし、vllm/ollamaなどに基づく自己構築DeepSeekモデルにも対応しています。また、プラグインメカニズムを通じてMCPModel Context Protocolサーバーをホストすることもでき、AI Agentが様々なツールやサービスを簡単に呼び出せるようにします。[openapi-to-mcpツール](https://github.com/higress-group/openapi-to-mcpserver)を使用すると、OpenAPI仕様を迅速にリモートMCPサーバーに変換してホスティングできます。HigressはLLM APIとMCP APIの統一管理を提供します。
Higressは、AIゲートウェイ機能を基盤に、Tongyi Qianwen APP、Bailian大規模モデルAPI、機械学習PAIプラットフォームなどのAIビジネスをサポートしています。また、国内の主要なAIGC企業ZeroOneやAI製品FastGPTにもサービスを提供しています
**🌟 今すぐ[https://mcp.higress.ai/](https://mcp.higress.ai/)で体験**してください。HigressがホストするリモートMCPサーバーを直接体験できます:
![](https://img.alicdn.com/imgextra/i2/O1CN011AbR8023V8R5N0HcA_!!6000000007260-2-tps-1080-606.png)
![Higress MCP Server Platform](https://img.alicdn.com/imgextra/i2/O1CN01nmVa0a1aChgpyyWOX_!!6000000003294-0-tps-3430-1742.jpg)
### 企業での採用
Higressは、Tengineのリロードが長時間接続のビジネスに影響を与える問題や、gRPC/Dubboの負荷分散能力の不足を解決するために、Alibaba内部で誕生しました。Alibaba Cloud内では、HigressのAIゲートウェイ機能がTongyi Qianwen APP、Tongyi Bailian Model Studio、機械学習PAIプラットフォームなどの中核的なAIアプリケーションをサポートしています。また、国内の主要なAIGC企業ZeroOneやAI製品FastGPTにもサービスを提供しています。Alibaba Cloudは、Higressを基盤にクラウドネイティブAPIゲートウェイ製品を構築し、多くの企業顧客に99.99%のゲートウェイ高可用性保証サービスを提供しています。
## 目次
@@ -73,6 +79,20 @@ K8sでのHelmデプロイなどの他のインストール方法については
![](https://img.alicdn.com/imgextra/i1/O1CN01fNnhCp1cV8mYPRFeS_!!6000000003605-0-tps-1080-608.jpg)
- **MCP Server ホスティング**:
Higressは、EnvoyベースのAPIゲートウェイとして、プラグインメカニズムを通じてMCP Serverをホストすることができます。MCPModel Context Protocolは本質的にAIにより親和性の高いAPIであり、AI Agentが様々なツールやサービスを簡単に呼び出せるようにします。Higressはツール呼び出しの認証、認可、レート制限、可観測性などの統一機能を提供し、AIアプリケーションの開発とデプロイを簡素化します。
![](https://img.alicdn.com/imgextra/i3/O1CN01K4qPUX1OliZa8KIPw_!!6000000001746-2-tps-1581-615.png)
Higressを使用してMCP Serverをホストすることで、以下のことが実現できます
- 統一された認証と認可メカニズム、AIツール呼び出しのセキュリティを確保
- きめ細かいレート制限、乱用やリソース枯渇を防止
- 包括的な監査ログ、すべてのツール呼び出し行動を記録
- 豊富な可観測性、ツール呼び出しのパフォーマンスと健全性を監視
- 簡素化されたデプロイと管理、Higressのプラグインメカニズムを通じて新しいMCP Serverを迅速に追加
- 動的更新による無停止Envoyの長時間接続に対する友好的なサポートとWasmプラグインの動的更新メカニズムにより、MCP Serverのロジックをリアルタイムで更新でき、トラフィックに完全に影響を与えず、接続が切断されることはありません
- **Kubernetes Ingressゲートウェイ**:
HigressはK8sクラスターのIngressエントリーポイントゲートウェイとして機能し、多くのK8s Nginx Ingressの注釈に対応しています。K8s Nginx IngressからHigressへのスムーズな移行が可能です。
@@ -203,4 +223,4 @@ WeChat公式アカウント
<a href="#readme-top" style="text-decoration: none; color: #007bff; font-weight: bold;">
↑ トップに戻る ↑
</a>
</p>
</p>

View File

@@ -11,7 +11,7 @@
[![Build Status](https://github.com/alibaba/higress/actions/workflows/build-and-test.yaml/badge.svg?branch=main)](https://github.com/alibaba/higress/actions)
[![license](https://img.shields.io/github/license/alibaba/higress.svg)](https://www.apache.org/licenses/LICENSE-2.0.html)
<a href="https://trendshift.io/repositories/10918" target="_blank"><img src="https://trendshift.io/api/badge/repositories/10918" alt="alibaba%2Fhigress | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://trendshift.io/repositories/10918" target="_blank"><img src="https://trendshift.io/api/badge/repositories/10918" alt="alibaba%2Fhigress | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a> <a href="https://www.producthunt.com/posts/higress?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-higress" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=951287&theme=light&t=1745492822283" alt="Higress - Global&#0032;APIs&#0032;as&#0032;MCP&#0032;powered&#0032;by&#0032;AI&#0032;Gateway | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
</div>
[**官网**](https://higress.cn/) &nbsp; |
@@ -28,15 +28,21 @@
</p>
## Higress 是什么?
Higress 是一款云原生 API 网关,内核基于 Istio 和 Envoy可以用 Go/Rust/JS 等编写 Wasm 插件提供了数十个现成的通用插件以及开箱即用的控制台demo 点[这里](http://demo.higress.io/)
Higress 在阿里内部为解决 Tengine reload 对长连接业务有损,以及 gRPC/Dubbo 负载均衡能力不足而诞生。
### 核心使用场景
阿里云基于 Higress 构建了云原生 API 网关产品,为大量企业客户提供 99.99% 的网关高可用保障服务能力
Higress 的 AI 网关能力支持国内外所有[主流模型供应商](https://github.com/alibaba/higress/tree/main/plugins/wasm-go/extensions/ai-proxy/provider)和基于 vllm/ollama 等自建的 DeepSeek 模型。同时Higress 支持通过插件方式托管 MCP (Model Context Protocol) 服务器,使 AI Agent 能够更容易地调用各种工具和服务。借助 [openapi-to-mcp 工具](https://github.com/higress-group/openapi-to-mcpserver),您可以快速将 OpenAPI 规范转换为远程 MCP 服务器进行托管。Higress 提供了对 LLM API 和 MCP API 的统一管理
Higress 的 AI 网关能力支持国内外所有[主流模型供应商](https://github.com/alibaba/higress/tree/main/plugins/wasm-go/extensions/ai-proxy/provider)和基于 vllm/ollama 等自建的 DeepSeek 模型;在阿里云内部支撑了通义千问 APP、百炼大模型 API、机器学习 PAI 平台等 AI 业务。同时服务国内头部的 AIGC 企业(如零一万物),以及 AI 产品(如 FastGPT
**🌟 立即体验 [https://mcp.higress.ai/](https://mcp.higress.ai/)** 基于 Higress 托管的远程 MCP 服务器:
![](https://img.alicdn.com/imgextra/i2/O1CN011AbR8023V8R5N0HcA_!!6000000007260-2-tps-1080-606.png)
![Higress MCP 服务器平台](https://img.alicdn.com/imgextra/i2/O1CN01nmVa0a1aChgpyyWOX_!!6000000003294-0-tps-3430-1742.jpg)
### 生产环境采用
Higress 在阿里内部为解决 Tengine reload 对长连接业务有损,以及 gRPC/Dubbo 负载均衡能力不足而诞生。在阿里云内部Higress 的 AI 网关能力支撑了通义千问 APP、通义百炼模型工作室、机器学习 PAI 平台等核心 AI 应用。同时服务国内头部的 AIGC 企业(如零一万物),以及 AI 产品(如 FastGPT。阿里云基于 Higress 构建了云原生 API 网关产品,为大量企业客户提供 99.99% 的网关高可用保障服务能力。
## Summary

View File

@@ -1 +1 @@
v2.1.1-rc.1
v2.1.2

View File

@@ -263,6 +263,14 @@ spec:
type: string
domain:
type: string
enableMCPServer:
type: boolean
mcpServerBaseUrl:
type: string
mcpServerExportDomains:
items:
type: string
type: array
nacosAccessKey:
type: string
nacosAddressServer:

View File

@@ -26,6 +26,8 @@
package v1
import (
_ "github.com/golang/protobuf/ptypes/struct"
wrappers "github.com/golang/protobuf/ptypes/wrappers"
_ "google.golang.org/genproto/googleapis/api/annotations"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
@@ -109,25 +111,28 @@ type RegistryConfig struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
Domain string `protobuf:"bytes,3,opt,name=domain,proto3" json:"domain,omitempty"`
Port uint32 `protobuf:"varint,4,opt,name=port,proto3" json:"port,omitempty"`
NacosAddressServer string `protobuf:"bytes,5,opt,name=nacosAddressServer,proto3" json:"nacosAddressServer,omitempty"`
NacosAccessKey string `protobuf:"bytes,6,opt,name=nacosAccessKey,proto3" json:"nacosAccessKey,omitempty"`
NacosSecretKey string `protobuf:"bytes,7,opt,name=nacosSecretKey,proto3" json:"nacosSecretKey,omitempty"`
NacosNamespaceId string `protobuf:"bytes,8,opt,name=nacosNamespaceId,proto3" json:"nacosNamespaceId,omitempty"`
NacosNamespace string `protobuf:"bytes,9,opt,name=nacosNamespace,proto3" json:"nacosNamespace,omitempty"`
NacosGroups []string `protobuf:"bytes,10,rep,name=nacosGroups,proto3" json:"nacosGroups,omitempty"`
NacosRefreshInterval int64 `protobuf:"varint,11,opt,name=nacosRefreshInterval,proto3" json:"nacosRefreshInterval,omitempty"`
ConsulNamespace string `protobuf:"bytes,12,opt,name=consulNamespace,proto3" json:"consulNamespace,omitempty"`
ZkServicesPath []string `protobuf:"bytes,13,rep,name=zkServicesPath,proto3" json:"zkServicesPath,omitempty"`
ConsulDatacenter string `protobuf:"bytes,14,opt,name=consulDatacenter,proto3" json:"consulDatacenter,omitempty"`
ConsulServiceTag string `protobuf:"bytes,15,opt,name=consulServiceTag,proto3" json:"consulServiceTag,omitempty"`
ConsulRefreshInterval int64 `protobuf:"varint,16,opt,name=consulRefreshInterval,proto3" json:"consulRefreshInterval,omitempty"`
AuthSecretName string `protobuf:"bytes,17,opt,name=authSecretName,proto3" json:"authSecretName,omitempty"`
Protocol string `protobuf:"bytes,18,opt,name=protocol,proto3" json:"protocol,omitempty"`
Sni string `protobuf:"bytes,19,opt,name=sni,proto3" json:"sni,omitempty"`
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
Domain string `protobuf:"bytes,3,opt,name=domain,proto3" json:"domain,omitempty"`
Port uint32 `protobuf:"varint,4,opt,name=port,proto3" json:"port,omitempty"`
NacosAddressServer string `protobuf:"bytes,5,opt,name=nacosAddressServer,proto3" json:"nacosAddressServer,omitempty"`
NacosAccessKey string `protobuf:"bytes,6,opt,name=nacosAccessKey,proto3" json:"nacosAccessKey,omitempty"`
NacosSecretKey string `protobuf:"bytes,7,opt,name=nacosSecretKey,proto3" json:"nacosSecretKey,omitempty"`
NacosNamespaceId string `protobuf:"bytes,8,opt,name=nacosNamespaceId,proto3" json:"nacosNamespaceId,omitempty"`
NacosNamespace string `protobuf:"bytes,9,opt,name=nacosNamespace,proto3" json:"nacosNamespace,omitempty"`
NacosGroups []string `protobuf:"bytes,10,rep,name=nacosGroups,proto3" json:"nacosGroups,omitempty"`
NacosRefreshInterval int64 `protobuf:"varint,11,opt,name=nacosRefreshInterval,proto3" json:"nacosRefreshInterval,omitempty"`
ConsulNamespace string `protobuf:"bytes,12,opt,name=consulNamespace,proto3" json:"consulNamespace,omitempty"`
ZkServicesPath []string `protobuf:"bytes,13,rep,name=zkServicesPath,proto3" json:"zkServicesPath,omitempty"`
ConsulDatacenter string `protobuf:"bytes,14,opt,name=consulDatacenter,proto3" json:"consulDatacenter,omitempty"`
ConsulServiceTag string `protobuf:"bytes,15,opt,name=consulServiceTag,proto3" json:"consulServiceTag,omitempty"`
ConsulRefreshInterval int64 `protobuf:"varint,16,opt,name=consulRefreshInterval,proto3" json:"consulRefreshInterval,omitempty"`
AuthSecretName string `protobuf:"bytes,17,opt,name=authSecretName,proto3" json:"authSecretName,omitempty"`
Protocol string `protobuf:"bytes,18,opt,name=protocol,proto3" json:"protocol,omitempty"`
Sni string `protobuf:"bytes,19,opt,name=sni,proto3" json:"sni,omitempty"`
McpServerExportDomains []string `protobuf:"bytes,20,rep,name=mcpServerExportDomains,proto3" json:"mcpServerExportDomains,omitempty"`
McpServerBaseUrl string `protobuf:"bytes,21,opt,name=mcpServerBaseUrl,proto3" json:"mcpServerBaseUrl,omitempty"`
EnableMCPServer *wrappers.BoolValue `protobuf:"bytes,22,opt,name=enableMCPServer,proto3" json:"enableMCPServer,omitempty"`
}
func (x *RegistryConfig) Reset() {
@@ -295,6 +300,27 @@ func (x *RegistryConfig) GetSni() string {
return ""
}
func (x *RegistryConfig) GetMcpServerExportDomains() []string {
if x != nil {
return x.McpServerExportDomains
}
return nil
}
func (x *RegistryConfig) GetMcpServerBaseUrl() string {
if x != nil {
return x.McpServerBaseUrl
}
return ""
}
func (x *RegistryConfig) GetEnableMCPServer() *wrappers.BoolValue {
if x != nil {
return x.EnableMCPServer
}
return nil
}
var File_networking_v1_mcp_bridge_proto protoreflect.FileDescriptor
var file_networking_v1_mcp_bridge_proto_rawDesc = []byte{
@@ -303,61 +329,76 @@ var file_networking_v1_mcp_bridge_proto_rawDesc = []byte{
0x12, 0x15, 0x68, 0x69, 0x67, 0x72, 0x65, 0x73, 0x73, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72,
0x6b, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f,
0x61, 0x70, 0x69, 0x2f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x62, 0x65, 0x68, 0x61, 0x76, 0x69,
0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x52, 0x0a, 0x09, 0x4d, 0x63, 0x70, 0x42,
0x72, 0x69, 0x64, 0x67, 0x65, 0x12, 0x45, 0x0a, 0x0a, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72,
0x69, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x68, 0x69, 0x67, 0x72,
0x65, 0x73, 0x73, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x2e, 0x76,
0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
0x52, 0x0a, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x69, 0x65, 0x73, 0x22, 0xd3, 0x05, 0x0a,
0x0e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12,
0x17, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x03, 0xe0,
0x41, 0x02, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65,
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x06,
0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x03, 0xe0, 0x41,
0x02, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x17, 0x0a, 0x04, 0x70, 0x6f, 0x72,
0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x04, 0x70, 0x6f,
0x72, 0x74, 0x12, 0x2e, 0x0a, 0x12, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x41, 0x64, 0x64, 0x72, 0x65,
0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12,
0x6e, 0x61, 0x63, 0x6f, 0x73, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76,
0x65, 0x72, 0x12, 0x26, 0x0a, 0x0e, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x41, 0x63, 0x63, 0x65, 0x73,
0x73, 0x4b, 0x65, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6e, 0x61, 0x63, 0x6f,
0x73, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x12, 0x26, 0x0a, 0x0e, 0x6e, 0x61,
0x63, 0x6f, 0x73, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x18, 0x07, 0x20, 0x01,
0x28, 0x09, 0x52, 0x0e, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4b,
0x65, 0x79, 0x12, 0x2a, 0x0a, 0x10, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x73,
0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x6e, 0x61,
0x63, 0x6f, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x26,
0x0a, 0x0e, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65,
0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x4e, 0x61, 0x6d,
0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x47,
0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x6e, 0x61, 0x63,
0x6f, 0x73, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x6e, 0x61, 0x63, 0x6f,
0x73, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c,
0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x14, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x52, 0x65, 0x66,
0x72, 0x65, 0x73, 0x68, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x28, 0x0a, 0x0f,
0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18,
0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x4e, 0x61, 0x6d,
0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x26, 0x0a, 0x0e, 0x7a, 0x6b, 0x53, 0x65, 0x72, 0x76,
0x69, 0x63, 0x65, 0x73, 0x50, 0x61, 0x74, 0x68, 0x18, 0x0d, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e,
0x7a, 0x6b, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x50, 0x61, 0x74, 0x68, 0x12, 0x2a,
0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x44, 0x61, 0x74, 0x61, 0x63, 0x65, 0x6e, 0x74,
0x65, 0x72, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c,
0x44, 0x61, 0x74, 0x61, 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x12, 0x2a, 0x0a, 0x10, 0x63, 0x6f,
0x6e, 0x73, 0x75, 0x6c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x54, 0x61, 0x67, 0x18, 0x0f,
0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x53, 0x65, 0x72, 0x76,
0x69, 0x63, 0x65, 0x54, 0x61, 0x67, 0x12, 0x34, 0x0a, 0x15, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c,
0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x18,
0x10, 0x20, 0x01, 0x28, 0x03, 0x52, 0x15, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x52, 0x65, 0x66,
0x72, 0x65, 0x73, 0x68, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x26, 0x0a, 0x0e,
0x61, 0x75, 0x74, 0x68, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x11,
0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74,
0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c,
0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c,
0x12, 0x10, 0x0a, 0x03, 0x73, 0x6e, 0x69, 0x18, 0x13, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x73,
0x6e, 0x69, 0x42, 0x2e, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
0x2f, 0x61, 0x6c, 0x69, 0x62, 0x61, 0x62, 0x61, 0x2f, 0x68, 0x69, 0x67, 0x72, 0x65, 0x73, 0x73,
0x2f, 0x61, 0x70, 0x69, 0x2f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x2f,
0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65,
0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x52, 0x0a, 0x09, 0x4d, 0x63, 0x70, 0x42, 0x72, 0x69,
0x64, 0x67, 0x65, 0x12, 0x45, 0x0a, 0x0a, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x69, 0x65,
0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x68, 0x69, 0x67, 0x72, 0x65, 0x73,
0x73, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e,
0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a,
0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x69, 0x65, 0x73, 0x22, 0xfd, 0x06, 0x0a, 0x0e, 0x52,
0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x17, 0x0a,
0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x03, 0xe0, 0x41, 0x02,
0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02,
0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x06, 0x64, 0x6f,
0x6d, 0x61, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52,
0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x17, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18,
0x04, 0x20, 0x01, 0x28, 0x0d, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74,
0x12, 0x2e, 0x0a, 0x12, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73,
0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x6e, 0x61,
0x63, 0x6f, 0x73, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72,
0x12, 0x26, 0x0a, 0x0e, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b,
0x65, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x41,
0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x12, 0x26, 0x0a, 0x0e, 0x6e, 0x61, 0x63, 0x6f,
0x73, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09,
0x52, 0x0e, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4b, 0x65, 0x79,
0x12, 0x2a, 0x0a, 0x10, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61,
0x63, 0x65, 0x49, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x6e, 0x61, 0x63, 0x6f,
0x73, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x26, 0x0a, 0x0e,
0x6e, 0x61, 0x63, 0x6f, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x09,
0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x73,
0x70, 0x61, 0x63, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x47, 0x72, 0x6f,
0x75, 0x70, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x6e, 0x61, 0x63, 0x6f, 0x73,
0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x52,
0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x18, 0x0b,
0x20, 0x01, 0x28, 0x03, 0x52, 0x14, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x52, 0x65, 0x66, 0x72, 0x65,
0x73, 0x68, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x28, 0x0a, 0x0f, 0x63, 0x6f,
0x6e, 0x73, 0x75, 0x6c, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x0c, 0x20,
0x01, 0x28, 0x09, 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x4e, 0x61, 0x6d, 0x65, 0x73,
0x70, 0x61, 0x63, 0x65, 0x12, 0x26, 0x0a, 0x0e, 0x7a, 0x6b, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
0x65, 0x73, 0x50, 0x61, 0x74, 0x68, 0x18, 0x0d, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, 0x7a, 0x6b,
0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x50, 0x61, 0x74, 0x68, 0x12, 0x2a, 0x0a, 0x10,
0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x44, 0x61, 0x74, 0x61, 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72,
0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x44, 0x61,
0x74, 0x61, 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x12, 0x2a, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x73,
0x75, 0x6c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x54, 0x61, 0x67, 0x18, 0x0f, 0x20, 0x01,
0x28, 0x09, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
0x65, 0x54, 0x61, 0x67, 0x12, 0x34, 0x0a, 0x15, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x52, 0x65,
0x66, 0x72, 0x65, 0x73, 0x68, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x18, 0x10, 0x20,
0x01, 0x28, 0x03, 0x52, 0x15, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x52, 0x65, 0x66, 0x72, 0x65,
0x73, 0x68, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x26, 0x0a, 0x0e, 0x61, 0x75,
0x74, 0x68, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x11, 0x20, 0x01,
0x28, 0x09, 0x52, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4e, 0x61,
0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x12,
0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x10,
0x0a, 0x03, 0x73, 0x6e, 0x69, 0x18, 0x13, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x73, 0x6e, 0x69,
0x12, 0x36, 0x0a, 0x16, 0x6d, 0x63, 0x70, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x45, 0x78, 0x70,
0x6f, 0x72, 0x74, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x14, 0x20, 0x03, 0x28, 0x09,
0x52, 0x16, 0x6d, 0x63, 0x70, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x45, 0x78, 0x70, 0x6f, 0x72,
0x74, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x2a, 0x0a, 0x10, 0x6d, 0x63, 0x70, 0x53,
0x65, 0x72, 0x76, 0x65, 0x72, 0x42, 0x61, 0x73, 0x65, 0x55, 0x72, 0x6c, 0x18, 0x15, 0x20, 0x01,
0x28, 0x09, 0x52, 0x10, 0x6d, 0x63, 0x70, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x42, 0x61, 0x73,
0x65, 0x55, 0x72, 0x6c, 0x12, 0x44, 0x0a, 0x0f, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x4d, 0x43,
0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x16, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e,
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0f, 0x65, 0x6e, 0x61, 0x62, 0x6c,
0x65, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x42, 0x2e, 0x5a, 0x2c, 0x67, 0x69,
0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x61, 0x6c, 0x69, 0x62, 0x61, 0x62, 0x61,
0x2f, 0x68, 0x69, 0x67, 0x72, 0x65, 0x73, 0x73, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x6e, 0x65, 0x74,
0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x33,
}
var (
@@ -374,16 +415,18 @@ func file_networking_v1_mcp_bridge_proto_rawDescGZIP() []byte {
var file_networking_v1_mcp_bridge_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_networking_v1_mcp_bridge_proto_goTypes = []interface{}{
(*McpBridge)(nil), // 0: higress.networking.v1.McpBridge
(*RegistryConfig)(nil), // 1: higress.networking.v1.RegistryConfig
(*McpBridge)(nil), // 0: higress.networking.v1.McpBridge
(*RegistryConfig)(nil), // 1: higress.networking.v1.RegistryConfig
(*wrappers.BoolValue)(nil), // 2: google.protobuf.BoolValue
}
var file_networking_v1_mcp_bridge_proto_depIdxs = []int32{
1, // 0: higress.networking.v1.McpBridge.registries:type_name -> higress.networking.v1.RegistryConfig
1, // [1:1] is the sub-list for method output_type
1, // [1:1] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
2, // 1: higress.networking.v1.RegistryConfig.enableMCPServer:type_name -> google.protobuf.BoolValue
2, // [2:2] is the sub-list for method output_type
2, // [2:2] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
}
func init() { file_networking_v1_mcp_bridge_proto_init() }

View File

@@ -15,6 +15,8 @@
syntax = "proto3";
import "google/api/field_behavior.proto";
import "google/protobuf/wrappers.proto";
import "google/protobuf/struct.proto";
// $schema: higress.networking.v1.McpBridge
// $title: McpBridge
@@ -66,4 +68,7 @@ message RegistryConfig {
string authSecretName = 17;
string protocol = 18;
string sni = 19;
repeated string mcpServerExportDomains = 20;
string mcpServerBaseUrl = 21;
google.protobuf.BoolValue enableMCPServer = 22;
}

39
go.mod
View File

@@ -39,7 +39,7 @@ require (
github.com/tidwall/gjson v1.17.0
go.uber.org/atomic v1.11.0
go.uber.org/zap v1.27.0
golang.org/x/net v0.27.0
golang.org/x/net v0.33.0
google.golang.org/genproto/googleapis/api v0.0.0-20230920204549-e6e6cdab5c13
google.golang.org/grpc v1.59.0
google.golang.org/protobuf v1.33.0
@@ -71,7 +71,27 @@ require (
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/alecholmes/xfccparser v0.1.0 // indirect
github.com/alecthomas/participle v0.4.1 // indirect
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1704 // indirect
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 // indirect
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
github.com/alibabacloud-go/darabonba-array v0.1.0 // indirect
github.com/alibabacloud-go/darabonba-encode-util v0.0.2 // indirect
github.com/alibabacloud-go/darabonba-map v0.0.2 // indirect
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10 // indirect
github.com/alibabacloud-go/darabonba-signature-util v0.0.7 // indirect
github.com/alibabacloud-go/darabonba-string v1.0.2 // indirect
github.com/alibabacloud-go/debug v1.0.1 // indirect
github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect
github.com/alibabacloud-go/kms-20160120/v3 v3.2.3 // indirect
github.com/alibabacloud-go/openapi-util v0.1.0 // indirect
github.com/alibabacloud-go/tea v1.2.2 // indirect
github.com/alibabacloud-go/tea-utils v1.4.4 // indirect
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect
github.com/alibabacloud-go/tea-xml v1.1.3 // indirect
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1800 // indirect
github.com/aliyun/alibabacloud-dkms-gcs-go-sdk v0.5.1 // indirect
github.com/aliyun/alibabacloud-dkms-transfer-go-sdk v0.1.8 // indirect
github.com/aliyun/aliyun-secretsmanager-client-go v1.1.5 // indirect
github.com/aliyun/credentials-go v1.4.3 // indirect
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect
github.com/armon/go-metrics v0.4.1 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
@@ -82,10 +102,12 @@ require (
github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/clbanning/mxj v1.8.4 // indirect
github.com/clbanning/mxj/v2 v2.5.5 // indirect
github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect
github.com/coreos/go-oidc/v3 v3.6.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/deckarep/golang-set v1.7.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/docker/cli v24.0.7+incompatible // indirect
github.com/docker/distribution v2.8.2+incompatible // indirect
@@ -165,6 +187,7 @@ require (
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc5 // indirect
github.com/openshift/api v0.0.0-20230720094506-afcbe27aec7c // indirect
github.com/orcaman/concurrent-map v0.0.0-20210501183033-44dafcb38ecc // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
@@ -182,6 +205,7 @@ require (
github.com/tetratelabs/wazero v1.7.3 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/toolkits/concurrent v0.0.0-20150624120057-a4371d70e3e3 // indirect
github.com/vbatts/tar-split v0.11.3 // indirect
github.com/xlab/treeprint v1.2.0 // indirect
@@ -197,14 +221,14 @@ require (
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/oauth2 v0.13.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/term v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/term v0.27.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
@@ -250,5 +274,6 @@ replace github.com/caddyserver/certmagic => github.com/2456868764/certmagic v1.0
replace (
github.com/dubbogo/gost => github.com/johnlanni/gost v1.11.23-0.20220713132522-0967a24036c6
github.com/nacos-group/nacos-sdk-go/v2 => github.com/luoxiner/nacos-sdk-go/v2 v2.2.9-60
golang.org/x/exp => golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1
)

144
go.sum
View File

@@ -683,9 +683,68 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/alessio/shellescape v1.2.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 h1:eIf+iGJxdU4U9ypaUfbtOWCsZSbTb8AUHvyPrxu6mAA=
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6/go.mod h1:4EUIoxs/do24zMOGGqYVWgw0s9NtiylnJglOeEB5UJo=
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc=
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 h1:zE8vH9C7JiZLNJJQ5OwjU9mSi4T9ef9u3BURT6LCLC8=
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5/go.mod h1:tWnyE9AjF8J8qqLk645oUmVUnFybApTQWklQmi5tY6g=
github.com/alibabacloud-go/darabonba-array v0.1.0 h1:vR8s7b1fWAQIjEjWnuF0JiKsCvclSRTfDzZHTYqfufY=
github.com/alibabacloud-go/darabonba-array v0.1.0/go.mod h1:BLKxr0brnggqOJPqT09DFJ8g3fsDshapUD3C3aOEFaI=
github.com/alibabacloud-go/darabonba-encode-util v0.0.2 h1:1uJGrbsGEVqWcWxrS9MyC2NG0Ax+GpOM5gtupki31XE=
github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F4PKuMgEUETNZasrDM6vqVr/Can7H8=
github.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc=
github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.9/go.mod h1:bb+Io8Sn2RuM3/Rpme6ll86jMyFSrD1bxeV/+v61KeU=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10 h1:GEYkMApgpKEVDn6z12DcH1EGYpDYRB8JxsazM4Rywak=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10/go.mod h1:26a14FGhZVELuz2cc2AolvW4RHmIO3/HRwsdHhaIPDE=
github.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg=
github.com/alibabacloud-go/darabonba-signature-util v0.0.7/go.mod h1:oUzCYV2fcCH797xKdL6BDH8ADIHlzrtKVjeRtunBNTQ=
github.com/alibabacloud-go/darabonba-string v1.0.2 h1:E714wms5ibdzCqGeYJ9JCFywE5nDyvIXIIQbZVFkkqo=
github.com/alibabacloud-go/darabonba-string v1.0.2/go.mod h1:93cTfV3vuPhhEwGGpKKqhVW4jLe7tDpo3LUM0i0g6mA=
github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY=
github.com/alibabacloud-go/debug v1.0.0/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
github.com/alibabacloud-go/debug v1.0.1 h1:MsW9SmUtbb1Fnt3ieC6NNZi6aEwrXfDksD4QA6GSbPg=
github.com/alibabacloud-go/debug v1.0.1/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
github.com/alibabacloud-go/endpoint-util v1.1.0 h1:r/4D3VSw888XGaeNpP994zDUaxdgTSHBbVfZlzf6b5Q=
github.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE=
github.com/alibabacloud-go/kms-20160120/v3 v3.2.3 h1:vamGcYQFwXVqR6RWcrVTTqlIXZVsYjaA7pZbx+Xw6zw=
github.com/alibabacloud-go/kms-20160120/v3 v3.2.3/go.mod h1:3rIyughsFDLie1ut9gQJXkWkMg/NfXBCk+OtXnPu3lw=
github.com/alibabacloud-go/openapi-util v0.1.0 h1:0z75cIULkDrdEhkLWgi9tnLe+KhAFE/r5Pb3312/eAY=
github.com/alibabacloud-go/openapi-util v0.1.0/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws=
github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg=
github.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
github.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
github.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
github.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
github.com/alibabacloud-go/tea v1.2.1/go.mod h1:qbzof29bM/IFhLMtJPrgTGK3eauV5J2wSyEUo4OEmnA=
github.com/alibabacloud-go/tea v1.2.2 h1:aTsR6Rl3ANWPfqeQugPglfurloyBJY85eFy7Gc1+8oU=
github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk=
github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE=
github.com/alibabacloud-go/tea-utils v1.4.4 h1:lxCDvNCdTo9FaXKKq45+4vGETQUKNOW/qKTcX9Sk53o=
github.com/alibabacloud-go/tea-utils v1.4.4/go.mod h1:KNcT0oXlZZxOXINnZBs6YvgOd5aYp9U67G+E3R8fcQw=
github.com/alibabacloud-go/tea-utils/v2 v2.0.3/go.mod h1:sj1PbjPodAVTqGTA3olprfeeqqmwD0A5OQz94o9EuXQ=
github.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4=
github.com/alibabacloud-go/tea-utils/v2 v2.0.6/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 h1:WDx5qW3Xa5ZgJ1c8NfqJkF6w+AU5wB8835UdhPr6Ax0=
github.com/alibabacloud-go/tea-utils/v2 v2.0.7/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
github.com/alibabacloud-go/tea-xml v1.1.3 h1:7LYnm+JbOq2B+T/B0fHC4Ies4/FofC4zHzYtqw7dgt0=
github.com/alibabacloud-go/tea-xml v1.1.3/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8=
github.com/aliyun/alibaba-cloud-sdk-go v1.61.18/go.mod h1:v8ESoHo4SyHmuB4b1tJqDHxfTGEciD+yhvOU/5s1Rfk=
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1704 h1:PpfENOj/vPfhhy9N2OFRjpue0hjM5XqAp2thFmkXXIk=
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1704/go.mod h1:RcDobYh8k5VP6TNybz9m++gL3ijVI5wueVr0EM10VsU=
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1800 h1:ie/8RxBOfKZWcrbYSJi2Z8uX8TcOlSMwPlEJh83OeOw=
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1800/go.mod h1:RcDobYh8k5VP6TNybz9m++gL3ijVI5wueVr0EM10VsU=
github.com/aliyun/alibabacloud-dkms-gcs-go-sdk v0.5.1 h1:nJYyoFP+aqGKgPs9JeZgS1rWQ4NndNR0Zfhh161ZltU=
github.com/aliyun/alibabacloud-dkms-gcs-go-sdk v0.5.1/go.mod h1:WzGOmFFTlUzXM03CJnHWMQ85UN6QGpOXZocCjwkiyOg=
github.com/aliyun/alibabacloud-dkms-transfer-go-sdk v0.1.8 h1:QeUdR7JF7iNCvO/81EhxEr3wDwxk4YBoYZOq6E0AjHI=
github.com/aliyun/alibabacloud-dkms-transfer-go-sdk v0.1.8/go.mod h1:xP0KIZry6i7oGPF24vhAPr1Q8vLZRcMcxtft5xDKwCU=
github.com/aliyun/aliyun-secretsmanager-client-go v1.1.5 h1:8S0mtD101RDYa0LXwdoqgN0RxdMmmJYjq8g2mk7/lQ4=
github.com/aliyun/aliyun-secretsmanager-client-go v1.1.5/go.mod h1:M19fxYz3gpm0ETnoKweYyYtqrtnVtrpKFpwsghbw+cQ=
github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw=
github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0=
github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM=
github.com/aliyun/credentials-go v1.3.10/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
github.com/aliyun/credentials-go v1.4.3 h1:N3iHyvHRMyOwY1+0qBLSf3hb5JFiOujVSVuEpgeGttY=
github.com/aliyun/credentials-go v1.4.3/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
@@ -755,7 +814,6 @@ github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6
github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.11-0.20170329064859-445be9e134b2/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
@@ -765,6 +823,8 @@ github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6D
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I=
github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng=
github.com/clbanning/mxj/v2 v2.5.5 h1:oT81vUeEiQQ/DcHbzSytRngP6Ky9O+L+0Bw0zSJag9E=
github.com/clbanning/mxj/v2 v2.5.5/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
@@ -813,6 +873,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deckarep/golang-set v1.7.1 h1:SCQV0S6gTtp6itiFrTqI+pfmJ4LN85S1YzhDf9rTHJQ=
github.com/deckarep/golang-set v1.7.1/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ=
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
@@ -1162,8 +1224,9 @@ github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97Dwqy
github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
@@ -1371,6 +1434,8 @@ github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhn
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
github.com/luoxiner/nacos-sdk-go/v2 v2.2.9-60 h1:FA/azfz2nSkMc1XR8LeqhcAiA/2/sOMcyBGYCTUc+Cs=
github.com/luoxiner/nacos-sdk-go/v2 v2.2.9-60/go.mod h1:9FKXl6FqOiVmm72i8kADtbeK71egyG9y3uRDBg41tpQ=
github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=
github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o=
github.com/lyft/protoc-gen-star/v2 v2.0.3/go.mod h1:amey7yeodaJhXSbf/TlLvWiqQfLOSpEk//mLlc+axEk=
@@ -1460,8 +1525,6 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/nacos-group/nacos-sdk-go v1.0.8 h1:8pEm05Cdav9sQgJSv5kyvlgfz0SzFUUGI3pWX6SiSnM=
github.com/nacos-group/nacos-sdk-go v1.0.8/go.mod h1:hlAPn3UdzlxIlSILAyOXKxjFSvDJ9oLzTJ9hLAK1KzA=
github.com/nacos-group/nacos-sdk-go/v2 v2.1.2 h1:A8GV6j0rw80I6tTKSav/pTpEgNECYXeFvZCsiLBWGnQ=
github.com/nacos-group/nacos-sdk-go/v2 v2.1.2/go.mod h1:ys/1adWeKXXzbNWfRNbaFlX/t6HVLWdpsNDvmoWTw0g=
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=
@@ -1517,6 +1580,8 @@ github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJ
github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/openzipkin/zipkin-go v0.3.0/go.mod h1:4c3sLeE8xjNqehmF5RpAFLPLJxXscc0R4l6Zg0P1tTQ=
github.com/orcaman/concurrent-map v0.0.0-20210501183033-44dafcb38ecc h1:Ak86L+yDSOzKFa7WM5bf5itSOo1e3Xh8bm5YCMUXIjQ=
github.com/orcaman/concurrent-map v0.0.0-20210501183033-44dafcb38ecc/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI=
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
@@ -1560,7 +1625,6 @@ github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3O
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.9.0/go.mod h1:FqZLKOZnGdFAhOK4nqGHa7D66IdsO+O441Eve7ptJDU=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
@@ -1593,7 +1657,6 @@ github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/prometheus/prometheus v0.45.0 h1:O/uG+Nw4kNxx/jDPxmjsSDd+9Ohql6E7ZSY1x5x/0KI=
@@ -1643,8 +1706,9 @@ github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v1.1.0 h1:MkTeG1DMwsrdH7QtLXy5W+fUxWq+vmb6cLmyJ7aRtF0=
github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
@@ -1713,6 +1777,9 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tmc/grpc-websocket-proxy v0.0.0-20200427203606-3cfed13b9966/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
@@ -1746,6 +1813,7 @@ github.com/yl2chen/cidranger v1.0.2 h1:lbOWZVCG1tCRX4u24kuM1Tb4nHqWkDxwLdoS+Seva
github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
@@ -1832,7 +1900,6 @@ go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0
go.uber.org/automaxprocs v1.4.0/go.mod h1:/mTEdr7LvHhs0v7mjdxDreTz1OG5zdZGqgOnhWiR/+Q=
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
@@ -1849,7 +1916,6 @@ go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@@ -1868,9 +1934,12 @@ golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
@@ -1882,8 +1951,13 @@ golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw=
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
@@ -1970,6 +2044,7 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
@@ -2008,8 +2083,13 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -2059,8 +2139,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -2107,6 +2187,7 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -2156,7 +2237,6 @@ golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -2181,8 +2261,13 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -2195,8 +2280,13 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -2214,8 +2304,11 @@ golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -2225,7 +2318,6 @@ golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxb
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
@@ -2279,6 +2371,7 @@ golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjs
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
@@ -2646,6 +2739,7 @@ gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=

View File

@@ -1,5 +1,5 @@
apiVersion: v2
appVersion: 2.1.1-rc.1
appVersion: 2.1.2
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.1-rc.1
version: 2.1.2

View File

@@ -1,9 +1,9 @@
dependencies:
- name: higress-core
repository: file://../core
version: 2.1.1-rc.1
version: 2.1.2
- name: higress-console
repository: https://higress.io/helm-charts/
version: 2.1.0
digest: sha256:a54678c246a6f0873cc4e9411902314f34d3aed439774d054c899574fdcba4cc
generated: "2025-04-14T21:04:25.733949+08:00"
version: 2.1.2
digest: sha256:7612de239141ca0d27400f7d5b9a786acd98826f511e2e3ed65ccd9d2c9f1700
generated: "2025-04-29T20:52:39.996652+08:00"

View File

@@ -1,5 +1,5 @@
apiVersion: v2
appVersion: 2.1.1-rc.1
appVersion: 2.1.2
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.1-rc.1
version: 2.1.2
- name: higress-console
repository: "https://higress.io/helm-charts/"
version: 2.1.0
version: 2.1.2
type: application
version: 2.1.1-rc.1
version: 2.1.2

View File

@@ -1,8 +1,8 @@
## Higress for Kubernetes
Higress 是基于阿里巴巴内部网关实践构建的云原生 API 网关。
Higress 是基于阿里巴巴内部网关实践的云原生 API 网关。
依托 Istio 和 EnvoyHigress 实现了流量网关、微服务网关和安全网关三重架构的融合,从而大降低了部署、运维成本。
通过基于 Istio 和 EnvoyHigress 实现了流量网关、微服务网关和安全网关三重网关架构的集成,从而大降低了部署、运维成本。
## 设置仓库信息
@@ -13,7 +13,7 @@ helm repo update
## 安装
`higress` 为发布名称安装 chart
使用名为 `higress` 的版本来安装 chart
```console
helm install higress -n higress-system higress.io/higress --create-namespace --render-subchart-notes
@@ -21,168 +21,60 @@ helm install higress -n higress-system higress.io/higress --create-namespace --r
## 卸载
卸载/删除 higress 部署:
卸载删除 higress 部署:
```console
helm delete higress -n higress-system
```
该命令会除与 chart 相关的所有 Kubernetes 组件并删除发
该命令会除与 chart 相关的所有 Kubernetes 组件并删除发行版
## 参数
## 值
## 配置
| 键 | 类型 | 默认值 | 描述 |
|-----|------|---------|-------------|
| clusterName | 字符串 | `""` | |
| controller.affinity | 对象 | `{}` | |
| controller.automaticHttps.email | 字符串 | `""` | |
| controller.automaticHttps.enabled | 布尔值 | `true` | |
| controller.autoscaling.enabled | 布尔值 | `false` | |
| controller.autoscaling.maxReplicas | 整数 | `5` | |
| controller.autoscaling.minReplicas | 整数 | `1` | |
| controller.autoscaling.targetCPUUtilizationPercentage | 整数 | `80` | |
| controller.env | 对象 | `{}` | |
| controller.hub | 字符串 | `"higress-registry.cn-hangzhou.cr.aliyuncs.com/higress"` | |
| controller.image | 字符串 | `"higress"` | |
| controller.imagePullSecrets | 列表 | `[]` | |
| controller.labels | 对象 | `{}` | |
| controller.name | 字符串 | `"higress-controller"` | |
| controller.nodeSelector | 对象 | `{}` | |
| controller.podAnnotations | 对象 | `{}` | |
| controller.podSecurityContext | 对象 | `{}` | |
| controller.ports[0].name | 字符串 | `"http"` | |
| controller.ports[0].port | 整数 | `8888` | |
| controller.ports[0].protocol | 字符串 | `"TCP"` | |
| controller.ports[0].targetPort | 整数 | `8888` | |
| controller.ports[1].name | 字符串 | `"http-solver"` | |
| controller.ports[1].port | 整数 | `8889` | |
| controller.ports[1].protocol | 字符串 | `"TCP"` | |
| controller.ports[1].targetPort | 整数 | `8889` | |
| controller.ports[2].name | 字符串 | `"grpc"` | |
| controller.ports[2].port | 整数 | `15051` | |
| controller.ports[2].protocol | 字符串 | `"TCP"` | |
| controller.ports[2].targetPort | 整数 | `15051` | |
| controller.probe.httpGet.path | 字符串 | `"/ready"` | |
| controller.probe.httpGet.port | 整数 | `8888` | |
| controller.probe.initialDelaySeconds | 整数 | `1` | |
| controller.probe.periodSeconds | 整数 | `3` | |
| controller.probe.timeoutSeconds | 整数 | `5` | |
| controller.rbac.create | 布尔值 | `true` | |
| controller.replicas | 整数 | `1` | Higress Controller 的 Pod 数量 |
| controller.resources.limits.cpu | 字符串 | `"1000m"` | |
| controller.resources.limits.memory | 字符串 | `"2048Mi"` | |
| controller.resources.requests.cpu | 字符串 | `"500m"` | |
| controller.resources.requests.memory | 字符串 | `"2048Mi"` | |
| controller.securityContext | 对象 | `{}` | |
| controller.service.type | 字符串 | `"ClusterIP"` | |
| controller.serviceAccount.annotations | 对象 | `{}` | 添加到服务账户的注解 |
| controller.serviceAccount.create | 布尔值 | `true` | 指定是否创建服务账户 |
| controller.serviceAccount.name | 字符串 | `""` | 如果未设置且 create 为 true则使用 fullname 模板生成名称 |
| controller.tag | 字符串 | `""` | |
| controller.tolerations | 列表 | `[]` | |
| downstream | 对象 | `{"connectionBufferLimits":32768,"http2":{"initialConnectionWindowSize":1048576,"initialStreamWindowSize":65535,"maxConcurrentStreams":100},"idleTimeout":180,"maxRequestHeadersKb":60,"routeTimeout":0}` | 下游配置设置 |
| gateway.affinity | 对象 | `{}` | |
| gateway.annotations | 对象 | `{}` | 应用到所有资源的注解 |
| gateway.autoscaling.enabled | 布尔值 | `false` | |
| gateway.autoscaling.maxReplicas | 整数 | `5` | |
| gateway.autoscaling.minReplicas | 整数 | `1` | |
| gateway.autoscaling.targetCPUUtilizationPercentage | 整数 | `80` | |
| gateway.containerSecurityContext | 字符串 | `nil` | |
| gateway.env | 对象 | `{}` | Pod 环境变量 |
| gateway.hostNetwork | 布尔值 | `false` | |
| gateway.httpPort | 整数 | `80` | |
| gateway.httpsPort | 整数 | `443` | |
| gateway.hub | 字符串 | `"higress-registry.cn-hangzhou.cr.aliyuncs.com/higress"` | |
| gateway.image | 字符串 | `"gateway"` | |
| gateway.kind | 字符串 | `"Deployment"` | 使用 `DaemonSet``Deployment` |
| gateway.labels | 对象 | `{}` | 应用到所有资源的标签 |
| gateway.metrics.enabled | 布尔值 | `false` | 如果为 true则为网关创建 PodMonitor 或 VMPodScrape |
| gateway.metrics.honorLabels | 布尔值 | `false` | |
| gateway.metrics.interval | 字符串 | `""` | |
| gateway.metrics.metricRelabelConfigs | 列表 | `[]` | 用于 operator.victoriametrics.com/v1beta1.VMPodScrape |
| gateway.metrics.metricRelabelings | 列表 | `[]` | 用于 monitoring.coreos.com/v1.PodMonitor |
| gateway.metrics.provider | 字符串 | `"monitoring.coreos.com"` | CustomResourceDefinition 的提供者组名,可以是 monitoring.coreos.com 或 operator.victoriametrics.com |
| gateway.metrics.rawSpec | 对象 | `{}` | 更多原始的 podMetricsEndpoints 规范 |
| gateway.metrics.relabelConfigs | 列表 | `[]` | |
| gateway.metrics.relabelings | 列表 | `[]` | |
| gateway.metrics.scrapeTimeout | 字符串 | `""` | |
| gateway.name | 字符串 | `"higress-gateway"` | |
| gateway.networkGateway | 字符串 | `""` | 如果指定,网关将作为给定网络的网络网关。 |
| gateway.nodeSelector | 对象 | `{}` | |
| gateway.podAnnotations."prometheus.io/path" | 字符串 | `"/stats/prometheus"` | |
| gateway.podAnnotations."prometheus.io/port" | 字符串 | `"15020"` | |
| gateway.podAnnotations."prometheus.io/scrape" | 字符串 | `"true"` | |
| gateway.podAnnotations."sidecar.istio.io/inject" | 字符串 | `"false"` | |
| gateway.rbac.enabled | 布尔值 | `true` | 如果启用,将创建角色以启用从网关访问证书。当使用 http://gateway-api.org/ 时不需要。 |
| gateway.readinessFailureThreshold | 整数 | `30` | 指示准备失败前的连续失败探测次数。 |
| gateway.readinessInitialDelaySeconds | 整数 | `1` | 准备探测的初始延迟秒数。 |
| gateway.readinessPeriodSeconds | 整数 | `2` | 准备探测之间的间隔。 |
| gateway.readinessSuccessThreshold | 整数 | `1` | 指示准备成功前的连续成功探测次数。 |
| gateway.readinessTimeoutSeconds | 整数 | `3` | 准备探测的超时秒数 |
| gateway.replicas | 整数 | `2` | Higress Gateway 的 Pod 数量 |
| gateway.resources.limits.cpu | 字符串 | `"2000m"` | |
| gateway.resources.limits.memory | 字符串 | `"2048Mi"` | |
| gateway.resources.requests.cpu | 字符串 | `"2000m"` | |
| gateway.resources.requests.memory | 字符串 | `"2048Mi"` | |
| gateway.revision | 字符串 | `""` | 修订声明此网关属于哪个修订 |
| gateway.rollingMaxSurge | 字符串 | `"100%"` | |
| gateway.rollingMaxUnavailable | 字符串 | `"25%"` | |
| gateway.securityContext | 字符串 | `nil` | 定义 Pod 的安全上下文。如果未设置,将自动设置为绑定到端口 80 和 443 所需的最小权限。在 Kubernetes 1.22+ 上,这只需要 `net.ipv4.ip_unprivileged_port_start` 系统调用。 |
| gateway.service.annotations | 对象 | `{}` | |
| gateway.service.externalTrafficPolicy | 字符串 | `""` | |
| gateway.service.loadBalancerClass | 字符串 | `""` | |
| gateway.service.loadBalancerIP | 字符串 | `""` | |
| gateway.service.loadBalancerSourceRanges | 列表 | `[]` | |
| gateway.service.ports[0].name | 字符串 | `"http2"` | |
| gateway.service.ports[0].port | 整数 | `80` | |
| gateway.service.ports[0].protocol | 字符串 | `"TCP"` | |
| gateway.service.ports[0].targetPort | 整数 | `80` | |
| gateway.service.ports[1].name | 字符串 | `"https"` | |
| gateway.service.ports[1].port | 整数 | `443` | |
| gateway.service.ports[1].protocol | 字符串 | `"TCP"` | |
| gateway.service.ports[1].targetPort | 整数 | `443` | |
| gateway.service.type | 字符串 | `"LoadBalancer"` | 服务类型。设置为 "None" 以完全禁用服务 |
| gateway.serviceAccount.annotations | 对象 | `{}` | 添加到服务账户的注解 |
| gateway.serviceAccount.create | 布尔值 | `true` | 如果设置,将创建服务账户。否则,使用默认值 |
| gateway.serviceAccount.name | 字符串 | `""` | 要使用的服务账户名称。如果未设置,则使用发布名称 |
| gateway.tag | 字符串 | `""` | |
| gateway.tolerations | 列表 | `[]` | |
| gateway.unprivilegedPortSupported | 字符串 | `nil` | |
| global.autoscalingv2API | 布尔值 | `true` | 是否使用 autoscaling/v2 模板进行 HPA 设置,仅供内部使用,用户不应配置。 |
| global.caAddress | 字符串 | `""` | 自定义的 CA 地址,用于为集群中的 Pod 检索证书。CSR 客户端(如 Istio Agent 和 ingress gateways可以使用此地址指定 CA 端点。如果未明确设置,则默认为 Istio 发现地址。 |
| global.caName | 字符串 | `""` | 工作负载证书的 CA 名称。例如,当 caName=GkeWorkloadCertificate 时GKE 工作负载证书将用作工作负载的证书。默认值为 "",当 caName="" 时CA 将通过其他机制(如环境变量 CA_PROVIDER配置。 |
| global.configCluster | 布尔值 | `false` | 将远程集群配置为外部 istiod 的配置集群。 |
| global.defaultPodDisruptionBudget | 对象 | `{"enabled":false}` | 为控制平面启用 Pod 中断预算,用于确保 Istio 控制平面组件逐步升级或恢复。 |
| global.defaultResources | 对象 | `{"requests":{"cpu":"10m"}}` | 应用于所有部署的最小请求资源集,以便 Horizontal Pod Autoscaler 能够正常工作(如果设置)。每个组件可以通过在相关部分添加自己的资源块并设置所需的资源值来覆盖这些默认值。 |
| global.defaultUpstreamConcurrencyThreshold | 整数 | `10000` | |
| global.disableAlpnH2 | 布尔值 | `false` | 是否在 ALPN 中禁用 HTTP/2 |
| global.enableGatewayAPI | 布尔值 | `false` | 如果为 trueHigress Controller 还将监控 Gateway API 资源 |
| global.enableH3 | 布尔值 | `false` | |
| global.enableIPv6 | 布尔值 | `false` | |
| global.enableIstioAPI | 布尔值 | `true` | 如果为 trueHigress Controller 还将监控 istio 资源 |
| global.enableLDSCache | 布尔值 | `true` | |
| global.enableProxyProtocol | 布尔值 | `false` | |
| global.enablePushAllMCPClusters | 布尔值 | `true` | |
| global.enableSRDS | 布尔值 | `true` | |
| global.enableStatus | 布尔值 | `true` | 如果为 trueHigress Controller 将更新 Ingress 资源的状态字段。从 Nginx Ingress 迁移时,为了避免 Ingress 对象的状态字段被覆盖,需要将此参数设置为 false以便 Higress 不会将入口 IP 写入相应 Ingress 对象的状态字段。 |
| global.externalIstiod | 布尔值 | `false` | 配置由外部 istiod 控制的远程集群数据平面。当设置为 true 时,本地不部署 istiod仅启用其他发现 chart 的子集。 |
| global.hostRDSMergeSubset | 布尔值 | `false` | |
| global.hub | 字符串 | `"higress-registry.cn-hangzhou.cr.aliyuncs.com/higress"` | Istio 镜像的默认仓库。发布版本发布到 docker hub 的 'istio' 项目下。来自 prow 的开发构建位于 gcr.io |
| global.imagePullPolicy | 字符串 | `""` | 如果不需要默认行为,则指定镜像拉取策略。默认行为:最新镜像将始终拉取,否则 IfNotPresent。 |
| global.imagePullSecrets | 列表 | `[]` | 所有 ServiceAccount 的 ImagePullSecrets用于引用此 ServiceAccount 的 Pod 拉取任何镜像的同一命名空间中的秘密列表。对于不使用 ServiceAccount 的组件(即 grafana、servicegraph、tracingImagePullSecrets 将添加到相应的 Deployment(StatefulSet) 对象中。对于配置了私有 docker 注册表的任何集群,必须设置。 |
| global.ingressClass | 字符串 | `"higress"` | IngressClass 过滤 higress controller 监听的 ingress 资源。默认的 ingress class 是 higress。有一些特殊情况用于特殊的 ingress class。1. 当 ingress class 设置为 nginx 时higress controller 将监听带有 nginx ingress class 或没有任何 ingress class 的 ingress 资源。2. 当 ingress class 设置为空时higress controller 将监听 k8s 集群中的所有 ingress 资源。 |
| global.istioNamespace | 字符串 | `"istio-system"` | 用于定位 istiod。 |
| global.istiod | 对象 | `{"enableAnalysis":false}` | 默认在主分支中启用以最大化测试。 |
| global.jwtPolicy | 字符串 | `"third-party-jwt"` | 配置验证 JWT 的策略。目前支持两个选项:"third-party-jwt" 和 "first-party-jwt"。 |
| global.kind | 布尔值 | `false` | |
| global.liteMetrics | 布尔值 | `false` | |
| global.local | 布尔值 | `false` | 当部署到本地集群kind 集群)时,将此设置为 true。 |
| global.logAsJson | 布尔值 | `false` | |
| global.logging | 对象 | `{"level":"default:info"}` | 以逗号分隔的每个范围的最小日志级别,格式为 <scope>:<level>,<scope>:<level> 控制平面根据组件不同有不同的范围,但可以配置所有组件的默认日志级别 如果为空,将使用代码中配置的默认范围和级别 |
| global.meshID | 字符串 | `""` | 如果网格管理员未指定值Istio 将使用网格的信任域的值。最佳实践是选择一个合适的信任域值。 |
| global.meshNetworks | 对象 | `{}` | |
| global.mountMtlsCerts | 布尔值 | `false` | 使用用户指定的、挂载的密钥和证书用于 Pilot 和工作负载。 |
| global.multiCluster.clusterName | 字符串 | `""` | 应设置为此安装运行的集群的名称。这是为了正确标记代理的 sidecar 注入所必需的 |
| global.multiCluster.enabled | 布尔值 | `true` | 设置为 true 以通过各自的 ingressgateway 服务连接两个 kubernetes 集群,当每个集群中的 Pod 无法直接相互通信时。
| 键 | 类型 | 默认值 | 描述 |
|------|------|---------|-------------|
| 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` | 上游连接缓冲限制(字节)|

View File

@@ -242,15 +242,15 @@ require (
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/oauth2 v0.13.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/term v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/term v0.27.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
google.golang.org/appengine v1.6.8 // indirect

View File

@@ -1789,8 +1789,9 @@ golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw=
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
@@ -1909,8 +1910,9 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1961,8 +1963,9 @@ golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -2069,8 +2072,9 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -2086,8 +2090,9 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -2108,8 +2113,9 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=

View File

@@ -22,5 +22,6 @@ var (
GatewayName = env.RegisterStringVar("GATEWAY_NAME", "higress-gateway", "").Get()
// Revision is the value of the Istio control plane revision, e.g. "canary",
// and is the value used by the "istio.io/rev" label.
Revision = env.Register("REVISION", "", "").Get()
Revision = env.Register("REVISION", "", "").Get()
McpServerWasmImageUrl = env.RegisterStringVar("MCP_SERVER_WASM_IMAGE_URL", "oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/mcp-server/all-in-one:1.0.0", "").Get()
)

View File

@@ -152,6 +152,7 @@ type IngressConfig struct {
httpsConfigMgr *cert.ConfigMgr
commonOptions common.Options
// templateProcessor processes template variables in config
templateProcessor *TemplateProcessor
@@ -197,6 +198,7 @@ func NewIngressConfig(localKubeClient kube.Client, xdsUpdater istiomodel.XDSUpda
namespace: namespace,
wasmPlugins: make(map[string]*extensions.WasmPlugin),
http2rpcs: make(map[string]*higressv1.Http2Rpc),
commonOptions: options,
}
// Initialize secret config manager
@@ -588,6 +590,13 @@ func (m *IngressConfig) convertVirtualService(configs []common.WrapperConfig) []
Spec: vs,
})
}
// add vs from naco3 for mcp server
if m.RegistryReconciler != nil {
allConfigsFromMcp := m.RegistryReconciler.GetAllConfigs(gvk.VirtualService)
for _, cfg := range allConfigsFromMcp {
out = append(out, *cfg)
}
}
// We generate some specific envoy filter here to avoid duplicated computation.
m.convertEnvoyFilter(&convertOptions)
@@ -674,6 +683,13 @@ func (m *IngressConfig) convertWasmPlugin([]common.WrapperConfig) []config.Confi
Spec: wasmPlugin,
})
}
// add wasm plugin from nacos for mcp server
if m.RegistryReconciler != nil {
wasmFromMcp := m.RegistryReconciler.GetAllConfigs(gvk.WasmPlugin)
for _, cfg := range wasmFromMcp {
out = append(out, *cfg)
}
}
return out
}
@@ -684,6 +700,7 @@ func (m *IngressConfig) convertServiceEntry([]common.WrapperConfig) []config.Con
serviceEntries := m.RegistryReconciler.GetAllServiceWrapper()
IngressLog.Infof("Found mcp serviceEntries %v", serviceEntries)
out := make([]config.Config, 0, len(serviceEntries))
hostSets := sets.Set[string]{}
for _, se := range serviceEntries {
out = append(out, config.Config{
Meta: config.Meta{
@@ -698,6 +715,15 @@ func (m *IngressConfig) convertServiceEntry([]common.WrapperConfig) []config.Con
},
Spec: se.ServiceEntry,
})
hostSets.Insert(se.ServiceEntry.Hosts[0])
}
// add service entry by host from nacos3 for mcp server
seFromMcp := m.RegistryReconciler.GetAllConfigs(gvk.ServiceEntry)
for _, cfg := range seFromMcp {
se := cfg.Spec.(*networking.ServiceEntry)
if !hostSets.Contains(se.Hosts[0]) {
out = append(out, *cfg)
}
}
return out
}
@@ -768,6 +794,10 @@ func (m *IngressConfig) convertDestinationRule(configs []common.WrapperConfig) [
if !exist {
destinationRules[serviceName] = destinationRuleWrapper
} else if dr.DestinationRule.TrafficPolicy != nil {
if dr.DestinationRule.TrafficPolicy.LoadBalancer == nil &&
destinationRuleWrapper.DestinationRule.TrafficPolicy.LoadBalancer != nil {
dr.DestinationRule.TrafficPolicy.LoadBalancer = destinationRuleWrapper.DestinationRule.TrafficPolicy.LoadBalancer
}
portTrafficPolicy := destinationRuleWrapper.DestinationRule.TrafficPolicy.PortLevelSettings[0]
portUpdated := false
for _, policy := range dr.DestinationRule.TrafficPolicy.PortLevelSettings {
@@ -904,7 +934,7 @@ func (m *IngressConfig) convertIstioWasmPlugin(obj *higressext.WasmPlugin) (*ext
result := &extensions.WasmPlugin{
Selector: &istiotype.WorkloadSelector{
MatchLabels: map[string]string{
"higress": m.namespace + "-higress-gateway",
m.commonOptions.GatewaySelectorKey: m.commonOptions.GatewaySelectorValue,
},
},
Url: obj.Url,
@@ -1135,6 +1165,28 @@ func (m *IngressConfig) AddOrUpdateMcpBridge(clusterNamespacedName util.ClusterN
// Set this label so that we do not compare configs and just push.
Labels: map[string]string{constants.AlwaysPushLabel: "true"},
}
vsMetadata := config.Meta{
Name: "mcpbridge-virtualservice",
Namespace: m.namespace,
GroupVersionKind: gvk.VirtualService,
// Set this label so that we do not compare configs and just push.
Labels: map[string]string{constants.AlwaysPushLabel: "true"},
}
wasmMetadata := config.Meta{
Name: "mcpbridge-wasmplugin",
Namespace: m.namespace,
GroupVersionKind: gvk.WasmPlugin,
// Set this label so that we do not compare configs and just push.
Labels: map[string]string{constants.AlwaysPushLabel: "true"},
}
efMetadata := config.Meta{
Name: "mcpbridge-envoyfilter",
Namespace: m.namespace,
GroupVersionKind: gvk.EnvoyFilter,
// Set this label so that we do not compare configs and just push.
Labels: map[string]string{constants.AlwaysPushLabel: "true"},
}
for _, f := range m.serviceEntryHandlers {
IngressLog.Debug("McpBridge triggerd serviceEntry update")
f(config.Config{Meta: seMetadata}, config.Config{Meta: seMetadata}, istiomodel.EventUpdate)
@@ -1143,9 +1195,22 @@ func (m *IngressConfig) AddOrUpdateMcpBridge(clusterNamespacedName util.ClusterN
IngressLog.Debug("McpBridge triggerd destinationRule update")
f(config.Config{Meta: drMetadata}, config.Config{Meta: drMetadata}, istiomodel.EventUpdate)
}
}, m.localKubeClient, m.namespace)
for _, f := range m.virtualServiceHandlers {
IngressLog.Debug("McpBridge triggerd virtualservice update")
f(config.Config{Meta: vsMetadata}, config.Config{Meta: vsMetadata}, istiomodel.EventUpdate)
}
for _, f := range m.wasmPluginHandlers {
IngressLog.Debug("McpBridge triggerd wasmplugin update")
f(config.Config{Meta: wasmMetadata}, config.Config{Meta: wasmMetadata}, istiomodel.EventUpdate)
}
for _, f := range m.envoyFilterHandlers {
IngressLog.Debug("McpBridge triggerd envoyfilter update")
f(config.Config{Meta: efMetadata}, config.Config{Meta: efMetadata}, istiomodel.EventUpdate)
}
}, m.localKubeClient, m.namespace, m.clusterId.String())
}
reconciler := m.RegistryReconciler
m.configmapMgr.SetMcpReconciler(m.RegistryReconciler)
err = reconciler.Reconcile(mcpbridge)
if err != nil {
IngressLog.Errorf("Mcpbridge reconcile failed, err:%v", err)

View File

@@ -0,0 +1,97 @@
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package common
import (
"testing"
"github.com/stretchr/testify/assert"
"istio.io/istio/pilot/pkg/model"
"istio.io/istio/pkg/config"
)
func TestIngressDomainCache(t *testing.T) {
cache := NewIngressDomainCache()
assert.NotNil(t, cache)
assert.NotNil(t, cache.Valid)
assert.Empty(t, cache.Invalid)
cache.Valid["example.com"] = &IngressDomainBuilder{
Host: "example.com",
Protocol: HTTP,
ClusterId: "cluster-1",
Ingress: &config.Config{
Meta: config.Meta{
Name: "test-ingress",
Namespace: "default",
},
},
}
cache.Invalid = append(cache.Invalid, model.IngressDomain{
Host: "invalid.com",
Error: "invalid domain",
})
result := cache.Extract()
assert.Equal(t, 1, len(result.Valid))
assert.Equal(t, "example.com", result.Valid[0].Host)
assert.Equal(t, string(HTTP), result.Valid[0].Protocol)
assert.Equal(t, 1, len(result.Invalid))
assert.Equal(t, "invalid.com", result.Invalid[0].Host)
}
func TestIngressDomainBuilder(t *testing.T) {
builder := &IngressDomainBuilder{
Host: "example.com",
Protocol: HTTP,
ClusterId: "cluster-1",
Ingress: &config.Config{
Meta: config.Meta{
Name: "test-ingress",
Namespace: "default",
},
},
}
domain := builder.Build()
assert.Equal(t, "example.com", domain.Host)
assert.Equal(t, string(HTTP), domain.Protocol)
builder.Event = MissingSecret
eventDomain := builder.Build()
assert.Contains(t, eventDomain.Error, "misses secret")
builder.Event = DuplicatedTls
builder.PreIngress = &config.Config{
Meta: config.Meta{
Name: "pre-ingress",
Namespace: "default",
},
}
builder.PreIngress.Meta.Annotations = map[string]string{
ClusterIdAnnotation: "pre-cluster",
}
dupDomain := builder.Build()
assert.Contains(t, dupDomain.Error, "conflicted with ingress")
builder.Protocol = HTTPS
builder.SecretName = "test-secret"
builder.Event = ""
httpsDomain := builder.Build()
assert.Equal(t, string(HTTPS), httpsDomain.Protocol)
assert.Equal(t, "test-secret", httpsDomain.SecretName)
}

View File

@@ -18,6 +18,7 @@ import (
"testing"
networking "istio.io/api/networking/v1alpha3"
"istio.io/istio/pilot/pkg/model"
"istio.io/istio/pkg/config"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -556,3 +557,514 @@ func TestSortHTTPRoutesWithMoreRules(t *testing.T) {
}
}
}
func TestValidateBackendResource(t *testing.T) {
groupStr := "networking.higress.io"
testCases := []struct {
name string
resource *v1.TypedLocalObjectReference
expected bool
}{
{
name: "nil resource",
resource: nil,
expected: false,
},
{
name: "nil APIGroup",
resource: &v1.TypedLocalObjectReference{
APIGroup: nil,
Kind: "McpBridge",
Name: "default",
},
expected: false,
},
{
name: "wrong APIGroup",
resource: &v1.TypedLocalObjectReference{
APIGroup: &groupStr,
Kind: "McpBridge",
Name: "wrong-name",
},
expected: false,
},
{
name: "wrong Kind",
resource: &v1.TypedLocalObjectReference{
APIGroup: &groupStr,
Kind: "WrongKind",
Name: "default",
},
expected: false,
},
{
name: "valid resource",
resource: &v1.TypedLocalObjectReference{
APIGroup: &groupStr,
Kind: "McpBridge",
Name: "default",
},
expected: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := ValidateBackendResource(tc.resource)
assert.Equal(t, tc.expected, result)
})
}
}
func TestCreateOrUpdateAnnotations(t *testing.T) {
testCases := []struct {
name string
annotations map[string]string
options Options
expected map[string]string
}{
{
name: "empty annotations",
annotations: map[string]string{},
options: Options{
ClusterId: "test-cluster",
RawClusterId: "raw-test-cluster",
},
expected: map[string]string{
ClusterIdAnnotation: "test-cluster",
RawClusterIdAnnotation: "raw-test-cluster",
},
},
{
name: "existing annotations",
annotations: map[string]string{
"key1": "value1",
"key2": "value2",
},
options: Options{
ClusterId: "test-cluster",
RawClusterId: "raw-test-cluster",
},
expected: map[string]string{
"key1": "value1",
"key2": "value2",
ClusterIdAnnotation: "test-cluster",
RawClusterIdAnnotation: "raw-test-cluster",
},
},
{
name: "overwrite existing cluster annotations",
annotations: map[string]string{
ClusterIdAnnotation: "old-cluster",
RawClusterIdAnnotation: "old-raw-cluster",
"key1": "value1",
},
options: Options{
ClusterId: "new-cluster",
RawClusterId: "new-raw-cluster",
},
expected: map[string]string{
ClusterIdAnnotation: "new-cluster",
RawClusterIdAnnotation: "new-raw-cluster",
"key1": "value1",
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := CreateOrUpdateAnnotations(tc.annotations, tc.options)
assert.Equal(t, tc.expected, result)
})
}
}
func TestGetClusterId(t *testing.T) {
testCases := []struct {
name string
annotations map[string]string
expected string
}{
{
name: "nil annotations",
annotations: nil,
expected: "",
},
{
name: "empty annotations",
annotations: map[string]string{},
expected: "",
},
{
name: "with cluster id",
annotations: map[string]string{
ClusterIdAnnotation: "test-cluster",
},
expected: "test-cluster",
},
{
name: "with other annotations",
annotations: map[string]string{
"key1": "value1",
"key2": "value2",
},
expected: "",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := GetClusterId(tc.annotations)
assert.Equal(t, tc.expected, string(result))
})
}
}
func TestConvertToDNSLabelValidAndCleanHost(t *testing.T) {
testCases := []struct {
name string
input string
}{
{
name: "simple host",
input: "example.com",
},
{
name: "wildcard host",
input: "*.example.com",
},
{
name: "long host",
input: "very-long-subdomain.example-service.my-namespace.svc.cluster.local",
},
{
name: "empty host",
input: "",
},
{
name: "ip address",
input: "192.168.1.1",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Test internal convertToDNSLabelValid function (through CleanHost)
result := CleanHost(tc.input)
// Validate result
assert.NotEmpty(t, result)
assert.Equal(t, 16, len(result)) // MD5 hash format is fixed length of 16 bytes
// Consistency check - same input should produce same output
result2 := CleanHost(tc.input)
assert.Equal(t, result, result2)
})
}
}
func TestSplitServiceFQDN(t *testing.T) {
testCases := []struct {
name string
fqdn string
expectedSvc string
expectedNs string
expectedValid bool
}{
{
name: "simple fqdn",
fqdn: "service.namespace",
expectedSvc: "service",
expectedNs: "namespace",
expectedValid: true,
},
{
name: "full k8s fqdn",
fqdn: "service.namespace.svc.cluster.local",
expectedSvc: "service",
expectedNs: "namespace",
expectedValid: true,
},
{
name: "just service name",
fqdn: "service",
expectedSvc: "",
expectedNs: "",
expectedValid: false,
},
{
name: "empty string",
fqdn: "",
expectedSvc: "",
expectedNs: "",
expectedValid: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
svc, ns, valid := SplitServiceFQDN(tc.fqdn)
assert.Equal(t, tc.expectedSvc, svc)
assert.Equal(t, tc.expectedNs, ns)
assert.Equal(t, tc.expectedValid, valid)
})
}
}
func TestConvertBackendService(t *testing.T) {
testCases := []struct {
name string
dest *networking.HTTPRouteDestination
expected model.BackendService
}{
{
name: "simple service",
dest: &networking.HTTPRouteDestination{
Destination: &networking.Destination{
Host: "service.namespace",
Port: &networking.PortSelector{
Number: 80,
},
},
Weight: 100,
},
expected: model.BackendService{
Name: "service",
Namespace: "namespace",
Port: 80,
Weight: 100,
},
},
{
name: "full k8s FQDN",
dest: &networking.HTTPRouteDestination{
Destination: &networking.Destination{
Host: "service.namespace.svc.cluster.local",
Port: &networking.PortSelector{
Number: 8080,
},
},
Weight: 50,
},
expected: model.BackendService{
Name: "service",
Namespace: "namespace",
Port: 8080,
Weight: 50,
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := ConvertBackendService(tc.dest)
assert.Equal(t, tc.expected.Name, result.Name)
assert.Equal(t, tc.expected.Namespace, result.Namespace)
assert.Equal(t, tc.expected.Port, result.Port)
assert.Equal(t, tc.expected.Weight, result.Weight)
})
}
}
func TestCreateConvertedName(t *testing.T) {
testCases := []struct {
name string
items []string
expected string
}{
{
name: "empty slice",
items: []string{},
expected: "",
},
{
name: "single item",
items: []string{"example"},
expected: "example",
},
{
name: "multiple items",
items: []string{"part1", "part2", "part3"},
expected: "part1-part2-part3",
},
{
name: "with empty strings",
items: []string{"part1", "", "part3"},
expected: "part1-part3",
},
{
name: "all empty strings",
items: []string{"", "", ""},
expected: "",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := CreateConvertedName(tc.items...)
assert.Equal(t, tc.expected, result)
})
}
}
func TestSortIngressByCreationTime(t *testing.T) {
configs := []config.Config{
{
Meta: config.Meta{
Name: "c-ingress",
Namespace: "ns1",
},
},
{
Meta: config.Meta{
Name: "a-ingress",
Namespace: "ns1",
},
},
{
Meta: config.Meta{
Name: "b-ingress",
Namespace: "ns1",
},
},
}
expected := []string{"a-ingress", "b-ingress", "c-ingress"}
SortIngressByCreationTime(configs)
var actual []string
for _, cfg := range configs {
actual = append(actual, cfg.Name)
}
assert.Equal(t, expected, actual, "When the timestamps are the same, the configuration should be sorted by name")
sameNamespaceConfigs := []config.Config{
{
Meta: config.Meta{
Name: "same-name",
Namespace: "c-ns",
},
},
{
Meta: config.Meta{
Name: "same-name",
Namespace: "a-ns",
},
},
{
Meta: config.Meta{
Name: "same-name",
Namespace: "b-ns",
},
},
}
expectedNamespace := []string{"a-ns", "b-ns", "c-ns"}
SortIngressByCreationTime(sameNamespaceConfigs)
var actualNamespace []string
for _, cfg := range sameNamespaceConfigs {
actualNamespace = append(actualNamespace, cfg.Namespace)
}
assert.Equal(t, expectedNamespace, actualNamespace, "When the names are the same, the configuration should be sorted by namespace")
}
func TestPartMd5(t *testing.T) {
testCases := []struct {
name string
input string
length int
}{
{
name: "empty string",
input: "",
length: 8,
},
{
name: "simple string",
input: "test",
length: 8,
},
{
name: "complex string",
input: "this-is-a-long-string-with-special-chars-!@#$%^&*()",
length: 8,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := partMd5(tc.input)
// Check result format
assert.Equal(t, tc.length, len(result), "MD5 hash excerpt should be 8 characters")
// Run twice to ensure deterministic output
result2 := partMd5(tc.input)
assert.Equal(t, result, result2, "partMd5 function should be deterministic")
})
}
}
func TestGetLbStatusListV1AndV1Beta1(t *testing.T) {
clusterPrefix = "gw-123-"
svcName := clusterPrefix
svcList := []*v1.Service{
{
ObjectMeta: metav1.ObjectMeta{
Name: svcName,
},
Spec: v1.ServiceSpec{
Type: v1.ServiceTypeLoadBalancer,
},
Status: v1.ServiceStatus{
LoadBalancer: v1.LoadBalancerStatus{
Ingress: []v1.LoadBalancerIngress{
{
IP: "2.2.2.2",
},
},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: svcName,
},
Spec: v1.ServiceSpec{
Type: v1.ServiceTypeLoadBalancer,
},
Status: v1.ServiceStatus{
LoadBalancer: v1.LoadBalancerStatus{
Ingress: []v1.LoadBalancerIngress{
{
Hostname: "1.1.1.1" + SvcHostNameSuffix,
},
},
},
},
},
}
// Test the V1 version
t.Run("GetLbStatusListV1", func(t *testing.T) {
lbiList := GetLbStatusListV1(svcList)
assert.Equal(t, 2, len(lbiList), "There should be 2 entry points")
assert.Equal(t, "1.1.1.1", lbiList[0].IP, "The first IP should be 1.1.1.1")
assert.Equal(t, "2.2.2.2", lbiList[1].IP, "The second IP should be 2.2.2.2")
})
// Test the V1Beta1 version
t.Run("GetLbStatusListV1Beta1", func(t *testing.T) {
lbiList := GetLbStatusListV1Beta1(svcList)
assert.Equal(t, 2, len(lbiList), "There should be 2 entry points")
assert.Equal(t, "1.1.1.1", lbiList[0].IP, "The first IP should be 1.1.1.1")
assert.Equal(t, "2.2.2.2", lbiList[1].IP, "The second IP should be 2.2.2.2")
})
}

View File

@@ -18,6 +18,7 @@ import (
"reflect"
"sync/atomic"
"github.com/alibaba/higress/registry/reconcile"
"istio.io/istio/pilot/pkg/model"
"istio.io/istio/pkg/cluster"
"istio.io/istio/pkg/config"
@@ -58,6 +59,7 @@ type ItemController interface {
ValidHigressConfig(higressConfig *HigressConfig) error
ConstructEnvoyFilters() ([]*config.Config, error)
RegisterItemEventHandler(eventHandler ItemEventHandler)
RegisterMcpReconciler(reconciler *reconcile.Reconciler)
}
type ConfigmapMgr struct {
@@ -111,6 +113,12 @@ func (c *ConfigmapMgr) GetHigressConfig() *HigressConfig {
return nil
}
func (c *ConfigmapMgr) SetMcpReconciler(reconciler *reconcile.Reconciler) {
for _, itemController := range c.ItemControllers {
itemController.RegisterMcpReconciler(reconciler)
}
}
func (c *ConfigmapMgr) AddItemControllers(controllers ...ItemController) {
c.ItemControllers = append(c.ItemControllers, controllers...)
}

View File

@@ -21,6 +21,7 @@ import (
"github.com/alibaba/higress/pkg/ingress/kube/util"
. "github.com/alibaba/higress/pkg/ingress/log"
"github.com/alibaba/higress/registry/reconcile"
networking "istio.io/api/networking/v1alpha3"
"istio.io/istio/pkg/config"
"istio.io/istio/pkg/config/schema/gvk"
@@ -376,6 +377,9 @@ func (g *GlobalOptionController) RegisterItemEventHandler(eventHandler ItemEvent
g.eventHandler = eventHandler
}
func (g *GlobalOptionController) RegisterMcpReconciler(reconciler *reconcile.Reconciler) {
}
// generateDownstreamEnvoyFilter generates the downstream envoy filter.
func (g *GlobalOptionController) generateDownstreamEnvoyFilter(downstreamValueStruct string, bufferLimitStruct string, routeTimeoutStruct string, namespace string) []*networking.EnvoyFilter_EnvoyConfigObjectPatch {
var downstreamConfig []*networking.EnvoyFilter_EnvoyConfigObjectPatch

View File

@@ -23,6 +23,7 @@ import (
"github.com/alibaba/higress/pkg/ingress/kube/util"
. "github.com/alibaba/higress/pkg/ingress/log"
"github.com/alibaba/higress/registry/reconcile"
networking "istio.io/api/networking/v1alpha3"
"istio.io/istio/pkg/config"
"istio.io/istio/pkg/config/schema/gvk"
@@ -291,6 +292,9 @@ func (g *GzipController) RegisterItemEventHandler(eventHandler ItemEventHandler)
g.eventHandler = eventHandler
}
func (g *GzipController) RegisterMcpReconciler(reconciler *reconcile.Reconciler) {
}
func (g *GzipController) constructGzipStruct(gzip *Gzip, namespace string) string {
gzipConfig := ""
contentType := ""

View File

@@ -24,6 +24,7 @@ import (
"github.com/alibaba/higress/pkg/ingress/kube/util"
. "github.com/alibaba/higress/pkg/ingress/log"
"github.com/alibaba/higress/registry/reconcile"
networking "istio.io/api/networking/v1alpha3"
"istio.io/istio/pkg/config"
"istio.io/istio/pkg/config/schema/gvk"
@@ -61,6 +62,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 +182,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{})
@@ -212,6 +216,7 @@ type McpServerController struct {
mcpServer atomic.Value
Name string
eventHandler ItemEventHandler
reconclier *reconcile.Reconciler
}
func NewMcpServerController(namespace string) *McpServerController {
@@ -285,6 +290,10 @@ func (m *McpServerController) RegisterItemEventHandler(eventHandler ItemEventHan
m.eventHandler = eventHandler
}
func (m *McpServerController) RegisterMcpReconciler(reconciler *reconcile.Reconciler) {
m.reconclier = reconciler
}
func (m *McpServerController) ConstructEnvoyFilters() ([]*config.Config, error) {
configs := make([]*config.Config, 0)
mcpServer := m.GetMcpServer()
@@ -294,88 +303,122 @@ 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 := "[]"
var matchConfigs []string
if len(mcp.MatchList) > 0 {
matchConfigs := make([]string, len(mcp.MatchList))
for i, rule := range mcp.MatchList {
matchConfigs[i] = fmt.Sprintf(`{
for _, rule := range mcp.MatchList {
matchConfigs = append(matchConfigs, fmt.Sprintf(`{
"match_rule_domain": "%s",
"match_rule_path": "%s",
"match_rule_type": "%s"
}`, rule.MatchRuleDomain, rule.MatchRulePath, rule.MatchRuleType)
}`, rule.MatchRuleDomain, rule.MatchRulePath, rule.MatchRuleType))
}
matchList = fmt.Sprintf("[%s]", strings.Join(matchConfigs, ","))
}
// 构建 Redis 配置
if m.reconclier != nil {
vsFromMcp := m.reconclier.GetAllConfigs(gvk.VirtualService)
for _, c := range vsFromMcp {
vs := c.Spec.(*networking.VirtualService)
var host string
if len(vs.Hosts) > 1 {
host = fmt.Sprintf("(%s)", strings.Join(vs.Hosts, "|"))
} else {
host = vs.Hosts[0]
}
path := vs.Http[0].Match[0].Uri.GetPrefix()
matchConfigs = append(matchConfigs, fmt.Sprintf(`{
"match_rule_domain": "%s",
"match_rule_path": "%s",
"match_rule_type": "prefix"
}`, host, path))
}
}
matchList = fmt.Sprintf("[%s]", strings.Join(matchConfigs, ","))
// Build redis configuration
redisConfig := "null"
if mcp.Redis != nil {
redisConfig = fmt.Sprintf(`{
@@ -386,7 +429,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 +460,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 +470,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)
})
}
}

View File

@@ -21,6 +21,7 @@ import (
"reflect"
"sync/atomic"
"github.com/alibaba/higress/registry/reconcile"
"istio.io/istio/pkg/config"
"istio.io/istio/pkg/config/schema/gvk"
@@ -237,6 +238,9 @@ func (t *TracingController) RegisterItemEventHandler(eventHandler ItemEventHandl
t.eventHandler = eventHandler
}
func (t *TracingController) RegisterMcpReconciler(reconciler *reconcile.Reconciler) {
}
func (t *TracingController) ConstructEnvoyFilters() ([]*config.Config, error) {
configs := make([]*config.Config, 0)
tracing := t.GetTracing()

View File

@@ -1,4 +1,4 @@
FROM golang:1.23-bullseye AS golang-base
FROM golang:1.22-bullseye AS golang-base
ARG GOPROXY
ARG GO_FILTER_NAME
@@ -24,7 +24,7 @@ WORKDIR /workspace
COPY . .
WORKDIR /workspace/$GO_FILTER_NAME
WORKDIR /workspace
RUN go mod tidy
RUN if [ "$GOARCH" = "arm64" ]; then \

View File

@@ -1,4 +1,4 @@
GO_FILTER_NAME ?= mcp-server
GO_FILTER_NAME ?= golang-filter
GOPROXY := $(shell go env GOPROXY)
GOARCH ?= amd64
@@ -8,5 +8,5 @@ build:
--build-arg GO_FILTER_NAME=${GO_FILTER_NAME} \
--build-arg GOARCH=${GOARCH} \
-t ${GO_FILTER_NAME} \
--output ./${GO_FILTER_NAME} \
--output . \
.

View File

@@ -28,7 +28,7 @@ http_filters:
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config
library_id: my-go-filter
library_path: "./my-go-filter.so"
library_path: "./go-filter.so"
plugin_name: my-go-filter
plugin_config:
"@type": type.googleapis.com/xds.type.v3.TypedStruct
@@ -43,5 +43,5 @@ http_filters:
使用以下命令可以快速构建 golang filter 插件:
```bash
GO_FILTER_NAME=mcp-server make build
make build
```

View File

@@ -1,6 +1,10 @@
module github.com/alibaba/higress/plugins/golang-filter/mcp-server
module github.com/alibaba/higress/plugins/golang-filter
go 1.23
go 1.22
replace github.com/envoyproxy/envoy => github.com/higress-group/envoy v0.0.0-20250428030521-17cf01d9f644
replace github.com/mark3labs/mcp-go => github.com/higress-group/mcp-go v0.0.0-20250428145706-792ce64b4b30
require (
github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42

View File

@@ -136,8 +136,6 @@ github.com/deckarep/golang-set v1.7.1 h1:SCQV0S6gTtp6itiFrTqI+pfmJ4LN85S1YzhDf9r
github.com/deckarep/golang-set v1.7.1/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/envoyproxy/envoy v1.33.1-0.20250325161043-11ab50a29d99 h1:jih/Ieb7BFgVCStgvY5fXQ3mI9ByOt4wfwUF0d7qmqI=
github.com/envoyproxy/envoy v1.33.1-0.20250325161043-11ab50a29d99/go.mod h1:x7d0dNbE0xGuDBUkBg19VGCgnPQ+lJ2k8lDzDzKExow=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@@ -236,6 +234,10 @@ 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/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=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
@@ -283,8 +285,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/luoxiner/nacos-sdk-go/v2 v2.2.9-40 h1:nzRTBplC0riQqQwEHZThw5H4/TH5LgWTQTm6A7t1lpY=
github.com/luoxiner/nacos-sdk-go/v2 v2.2.9-40/go.mod h1:9FKXl6FqOiVmm72i8kADtbeK71egyG9y3uRDBg41tpQ=
github.com/mark3labs/mcp-go v0.12.0 h1:Pue1Tdwqcz77GHq18uzgmLT3wmeDUxXUSAqSwhGLhVo=
github.com/mark3labs/mcp-go v0.12.0/go.mod h1:cjMlBU0cv/cj9kjlgmRhoJ5JREdS7YX83xeIG9Ko/jE=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=

View File

@@ -0,0 +1,25 @@
package main
import (
"net/http"
mcp_server "github.com/alibaba/higress/plugins/golang-filter/mcp-server"
mcp_session "github.com/alibaba/higress/plugins/golang-filter/mcp-session"
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
envoyHttp "github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go/pkg/http"
)
func init() {
envoyHttp.RegisterHttpFilterFactoryAndConfigParser(mcp_session.Name, mcp_session.FilterFactory, &mcp_session.Parser{})
envoyHttp.RegisterHttpFilterFactoryAndConfigParser(mcp_server.Name, mcp_server.FilterFactory, &mcp_server.Parser{})
go func() {
defer func() {
if r := recover(); r != nil {
api.LogErrorf("PProf server recovered from panic: %v", r)
}
}()
api.LogError(http.ListenAndServe("localhost:6060", nil).Error())
}()
}
func main() {}

View File

@@ -1,61 +1,39 @@
package main
package mcp_server
import (
"fmt"
"net/http"
_ "net/http/pprof"
xds "github.com/cncf/xds/go/xds/type/v3"
"google.golang.org/protobuf/types/known/anypb"
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/handler"
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
_ "github.com/alibaba/higress/plugins/golang-filter/mcp-server/registry/nacos"
_ "github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/gorm"
mcp_session "github.com/alibaba/higress/plugins/golang-filter/mcp-session"
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
xds "github.com/cncf/xds/go/xds/type/v3"
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
envoyHttp "github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go/pkg/http"
"google.golang.org/protobuf/types/known/anypb"
)
const Name = "mcp-session"
const Name = "mcp-server"
const Version = "1.0.0"
const DefaultServerName = "defaultServer"
const ConfigPathSuffix = "/config"
func init() {
envoyHttp.RegisterHttpFilterFactoryAndConfigParser(Name, filterFactory, &parser{})
go func() {
defer func() {
if r := recover(); r != nil {
api.LogErrorf("PProf server recovered from panic: %v", r)
}
}()
api.LogError(http.ListenAndServe("localhost:6060", nil).Error())
}()
type SSEServerWrapper struct {
BaseServer *common.SSEServer
DomainList []string
}
type config struct {
ssePathSuffix string
redisClient *internal.RedisClient
servers []*internal.SSEServer
defaultServer *internal.SSEServer
matchList []internal.MatchRule
enableUserLevelServer bool
rateLimitConfig *handler.MCPRatelimitConfig
servers []*SSEServerWrapper
}
func (c *config) Destroy() {
if c.redisClient != nil {
api.LogDebug("Closing Redis client")
c.redisClient.Close()
for _, server := range c.servers {
server.BaseServer.Close()
}
}
type parser struct {
type Parser struct {
}
// Parse the filter configuration
func (p *parser) Parse(any *anypb.Any, callbacks api.ConfigCallbackHandler) (interface{}, error) {
func (p *Parser) Parse(any *anypb.Any, callbacks api.ConfigCallbackHandler) (interface{}, error) {
configStruct := &xds.TypedStruct{}
if err := any.UnmarshalTo(configStruct); err != nil {
return nil, err
@@ -63,79 +41,9 @@ func (p *parser) Parse(any *anypb.Any, callbacks api.ConfigCallbackHandler) (int
v := configStruct.Value
conf := &config{
matchList: make([]internal.MatchRule, 0),
servers: make([]*internal.SSEServer, 0),
servers: make([]*SSEServerWrapper, 0),
}
// Parse match_list if exists
if matchList, ok := v.AsMap()["match_list"].([]interface{}); ok {
for _, item := range matchList {
if ruleMap, ok := item.(map[string]interface{}); ok {
rule := internal.MatchRule{}
if domain, ok := ruleMap["match_rule_domain"].(string); ok {
rule.MatchRuleDomain = domain
}
if path, ok := ruleMap["match_rule_path"].(string); ok {
rule.MatchRulePath = path
}
if ruleType, ok := ruleMap["match_rule_type"].(string); ok {
rule.MatchRuleType = internal.RuleType(ruleType)
}
conf.matchList = append(conf.matchList, rule)
}
}
}
// Redis configuration is optional
if redisConfigMap, ok := v.AsMap()["redis"].(map[string]interface{}); ok {
redisConfig, err := internal.ParseRedisConfig(redisConfigMap)
if err != nil {
return nil, fmt.Errorf("failed to parse redis config: %w", err)
}
redisClient, err := internal.NewRedisClient(redisConfig)
if err != nil {
return nil, fmt.Errorf("failed to initialize RedisClient: %w", err)
}
conf.redisClient = redisClient
api.LogDebug("Redis client initialized")
} else {
api.LogDebug("Redis configuration not provided, running without Redis")
}
enableUserLevelServer, ok := v.AsMap()["enable_user_level_server"].(bool)
if !ok {
enableUserLevelServer = false
if conf.redisClient == nil {
return nil, fmt.Errorf("redis configuration is not provided, enable_user_level_server is true")
}
}
conf.enableUserLevelServer = enableUserLevelServer
if rateLimit, ok := v.AsMap()["rate_limit"].(map[string]interface{}); ok {
rateLimitConfig := &handler.MCPRatelimitConfig{}
if limit, ok := rateLimit["limit"].(float64); ok {
rateLimitConfig.Limit = int(limit)
}
if window, ok := rateLimit["window"].(float64); ok {
rateLimitConfig.Window = int(window)
}
if whiteList, ok := rateLimit["white_list"].([]interface{}); ok {
for _, item := range whiteList {
if uid, ok := item.(string); ok {
rateLimitConfig.Whitelist = append(rateLimitConfig.Whitelist, uid)
}
}
}
conf.rateLimitConfig = rateLimitConfig
}
ssePathSuffix, ok := v.AsMap()["sse_path_suffix"].(string)
if !ok || ssePathSuffix == "" {
return nil, fmt.Errorf("sse path suffix is not set or empty")
}
conf.ssePathSuffix = ssePathSuffix
serverConfigs, ok := v.AsMap()["servers"].([]interface{})
if !ok {
api.LogDebug("No servers are configured")
@@ -147,19 +55,33 @@ func (p *parser) Parse(any *anypb.Any, callbacks api.ConfigCallbackHandler) (int
if !ok {
return nil, fmt.Errorf("server config must be an object")
}
serverType, ok := serverConfigMap["type"].(string)
if !ok {
return nil, fmt.Errorf("server type is not set")
}
serverPath, ok := serverConfigMap["path"].(string)
if !ok {
return nil, fmt.Errorf("server %s path is not set", serverType)
}
serverDomainList := []string{}
if domainList, ok := serverConfigMap["domain_list"].([]interface{}); ok {
for _, domain := range domainList {
if domainStr, ok := domain.(string); ok {
serverDomainList = append(serverDomainList, domainStr)
}
}
} else {
serverDomainList = []string{"*"}
}
serverName, ok := serverConfigMap["name"].(string)
if !ok {
return nil, fmt.Errorf("server %s name is not set", serverType)
}
server := internal.GlobalRegistry.GetServer(serverType)
server := common.GlobalRegistry.GetServer(serverType)
if server == nil {
return nil, fmt.Errorf("server %s is not registered", serverType)
@@ -180,50 +102,36 @@ func (p *parser) Parse(any *anypb.Any, callbacks api.ConfigCallbackHandler) (int
return nil, fmt.Errorf("failed to initialize DBServer: %w", err)
}
conf.servers = append(conf.servers, internal.NewSSEServer(serverInstance,
internal.WithRedisClient(conf.redisClient),
internal.WithSSEEndpoint(fmt.Sprintf("%s%s", serverPath, ssePathSuffix)),
internal.WithMessageEndpoint(serverPath)))
conf.servers = append(conf.servers, &SSEServerWrapper{
BaseServer: common.NewSSEServer(serverInstance,
common.WithSSEEndpoint(fmt.Sprintf("%s%s", serverPath, mcp_session.GlobalSSEPathSuffix)),
common.WithMessageEndpoint(serverPath)),
DomainList: serverDomainList,
})
api.LogDebug(fmt.Sprintf("Registered MCP Server: %s", serverType))
}
return conf, nil
}
func (p *parser) Merge(parent interface{}, child interface{}) interface{} {
func (p *Parser) Merge(parent interface{}, child interface{}) interface{} {
parentConfig := parent.(*config)
childConfig := child.(*config)
newConfig := *parentConfig
if childConfig.redisClient != nil {
newConfig.redisClient = childConfig.redisClient
}
if childConfig.ssePathSuffix != "" {
newConfig.ssePathSuffix = childConfig.ssePathSuffix
}
if childConfig.servers != nil {
newConfig.servers = childConfig.servers
}
if childConfig.defaultServer != nil {
newConfig.defaultServer = childConfig.defaultServer
}
if childConfig.matchList != nil {
newConfig.matchList = childConfig.matchList
}
return &newConfig
}
func filterFactory(c interface{}, callbacks api.FilterCallbackHandler) api.StreamFilter {
func FilterFactory(c interface{}, callbacks api.FilterCallbackHandler) api.StreamFilter {
conf, ok := c.(*config)
if !ok {
panic("unexpected config type")
}
return &filter{
callbacks: callbacks,
config: conf,
stopChan: make(chan struct{}),
mcpConfigHandler: handler.NewMCPConfigHandler(conf.redisClient, callbacks),
mcpRatelimitHandler: handler.NewMCPRatelimitHandler(conf.redisClient, callbacks, conf.rateLimitConfig),
config: conf,
callbacks: callbacks,
}
}
func main() {}

View File

@@ -1,101 +1,44 @@
package main
package mcp_server
import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"strings"
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/handler"
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
)
const (
RedisNotEnabledResponseBody = "Redis is not enabled, SSE connection is not supported"
)
// The callbacks in the filter, like `DecodeHeaders`, can be implemented on demand.
// Because api.PassThroughStreamFilter provides a default implementation.
type filter struct {
api.PassThroughStreamFilter
callbacks api.FilterCallbackHandler
path string
config *config
stopChan chan struct{}
req *http.Request
serverName string
message bool
proxyURL *url.URL
skip bool
userLevelConfig bool
mcpConfigHandler *handler.MCPConfigHandler
mcpRatelimitHandler *handler.MCPRatelimitHandler
config *config
req *http.Request
message bool
path string
}
type RequestURL struct {
method string
scheme string
host string
path string
baseURL string
parsedURL *url.URL
internalIP bool
}
func NewRequestURL(header api.RequestHeaderMap) *RequestURL {
method, _ := header.Get(":method")
scheme, _ := header.Get(":scheme")
host, _ := header.Get(":authority")
path, _ := header.Get(":path")
internalIP, _ := header.Get("x-envoy-internal")
baseURL := fmt.Sprintf("%s://%s", scheme, host)
parsedURL, _ := url.Parse(path)
api.LogDebugf("RequestURL: method=%s, scheme=%s, host=%s, path=%s", method, scheme, host, path)
return &RequestURL{method: method, scheme: scheme, host: host, path: path, baseURL: baseURL, parsedURL: parsedURL, internalIP: internalIP == "true"}
}
// 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 := NewRequestURL(header)
f.path = url.parsedURL.Path
// Check if request matches any rule in match_list
if !internal.IsMatch(f.config.matchList, url.host, f.path) {
f.skip = true
api.LogDebugf("Request does not match any rule in match_list: %s", url.parsedURL.String())
url := common.NewRequestURL(header)
if url == nil {
return api.Continue
}
f.path = url.ParsedURL.Path
for _, server := range f.config.servers {
if f.path == server.GetSSEEndpoint() {
if url.method != http.MethodGet {
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusMethodNotAllowed, "Method not allowed", nil, 0, "")
} else {
f.serverName = server.GetServerName()
body := "SSE connection create"
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusOK, body, nil, 0, "")
}
api.LogDebugf("%s SSE connection started", server.GetServerName())
return api.LocalReply
} else if f.path == server.GetMessageEndpoint() {
if url.method != http.MethodPost {
if common.MatchDomainList(url.ParsedURL.Host, server.DomainList) && url.ParsedURL.Path == server.BaseServer.GetMessageEndpoint() {
if url.Method != http.MethodPost {
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusMethodNotAllowed, "Method not allowed", nil, 0, "")
return api.LocalReply
}
// Create a new http.Request object
f.req = &http.Request{
Method: url.method,
URL: url.parsedURL,
Method: url.Method,
URL: url.ParsedURL,
Header: make(http.Header),
}
api.LogDebugf("Message request: %v", url.parsedURL)
api.LogDebugf("Message request: %v", url.ParsedURL)
// Copy headers from api.RequestHeaderMap to http.Header
header.Range(func(key, value string) bool {
f.req.Header.Add(key, value)
@@ -110,180 +53,33 @@ func (f *filter) DecodeHeaders(header api.RequestHeaderMap, endStream bool) api.
}
}
if strings.HasSuffix(f.path, ConfigPathSuffix) && f.config.enableUserLevelServer {
if !url.internalIP {
api.LogWarnf("Access denied: non-internal IP address %s", url.parsedURL.String())
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusForbidden, "", nil, 0, "")
return api.LocalReply
}
if strings.HasSuffix(f.path, ConfigPathSuffix) && url.method == http.MethodGet {
api.LogDebugf("Handling config request: %s", f.path)
f.mcpConfigHandler.HandleConfigRequest(f.path, url.method, []byte{})
return api.LocalReply
}
f.req = &http.Request{
Method: url.method,
URL: url.parsedURL,
}
f.userLevelConfig = true
if endStream {
return api.Continue
} else {
return api.StopAndBuffer
}
}
if !strings.HasSuffix(url.parsedURL.Path, f.config.ssePathSuffix) {
f.proxyURL = url.parsedURL
if f.config.enableUserLevelServer {
parts := strings.Split(url.parsedURL.Path, "/")
if len(parts) < 3 {
api.LogDebugf("Access denied: missing uid in path %s", url.parsedURL.Path)
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusForbidden, "Access denied: missing uid", nil, 0, "")
return api.LocalReply
}
serverName := parts[1]
uid := parts[2]
// Get encoded config
encodedConfig, err := f.mcpConfigHandler.GetEncodedConfig(serverName, uid)
if err != nil {
api.LogWarnf("Access denied: no valid config found for uid %s", uid)
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusForbidden, "", nil, 0, "")
return api.LocalReply
} else if encodedConfig != "" {
header.Set("x-higress-mcpserver-config", encodedConfig)
api.LogDebugf("Set x-higress-mcpserver-config Header for %s:%s", serverName, uid)
} else {
api.LogDebugf("Empty config found for %s:%s", serverName, uid)
if !f.mcpRatelimitHandler.HandleRatelimit(url.parsedURL.Path, url.method, []byte{}) {
return api.LocalReply
}
}
}
return api.Continue
}
if url.method != http.MethodGet {
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusMethodNotAllowed, "Method not allowed", nil, 0, "")
} else {
f.config.defaultServer = internal.NewSSEServer(internal.NewMCPServer(DefaultServerName, Version),
internal.WithSSEEndpoint(f.config.ssePathSuffix),
internal.WithMessageEndpoint(strings.TrimSuffix(url.parsedURL.Path, f.config.ssePathSuffix)),
internal.WithRedisClient(f.config.redisClient))
f.serverName = f.config.defaultServer.GetServerName()
body := "SSE connection create"
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusOK, body, nil, 0, "")
}
return api.LocalReply
return api.Continue
}
// 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.skip {
return api.Continue
}
if f.message {
if endStream {
for _, server := range f.config.servers {
if f.path == server.GetMessageEndpoint() {
// Create a response recorder to capture the response
recorder := httptest.NewRecorder()
// Call the handleMessage method of SSEServer with complete body
server.HandleMessage(recorder, f.req, buffer.Bytes())
f.message = false
f.callbacks.DecoderFilterCallbacks().SendLocalReply(recorder.Code, recorder.Body.String(), recorder.Header(), 0, "")
return api.LocalReply
}
}
}
return api.StopAndBuffer
} else if f.userLevelConfig {
// Handle config POST request
api.LogDebugf("Handling config request: %s", f.path)
f.mcpConfigHandler.HandleConfigRequest(f.path, f.req.Method, buffer.Bytes())
return api.LocalReply
}
return api.Continue
}
// 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.skip {
return api.Continue
}
if f.serverName != "" {
if f.config.redisClient != nil {
header.Set("Content-Type", "text/event-stream")
header.Set("Cache-Control", "no-cache")
header.Set("Connection", "keep-alive")
header.Set("Access-Control-Allow-Origin", "*")
header.Del("Content-Length")
} else {
header.Set("Content-Length", strconv.Itoa(len(RedisNotEnabledResponseBody)))
}
return api.Continue
}
return api.Continue
}
// 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.skip {
return api.Continue
}
if !endStream {
return api.StopAndBuffer
}
if f.proxyURL != nil && f.config.redisClient != nil {
sessionID := f.proxyURL.Query().Get("sessionId")
if sessionID != "" {
channel := internal.GetSSEChannelName(sessionID)
eventData := fmt.Sprintf("event: message\ndata: %s\n\n", buffer.String())
publishErr := f.config.redisClient.Publish(channel, eventData)
if publishErr != nil {
api.LogErrorf("Failed to publish wasm mcp server message to Redis: %v", publishErr)
if f.message {
for _, server := range f.config.servers {
if f.path == server.BaseServer.GetMessageEndpoint() {
// Create a response recorder to capture the response
recorder := httptest.NewRecorder()
// Call the handleMessage method of SSEServer with complete body
httpStatus := server.BaseServer.HandleMessage(recorder, f.req, buffer.Bytes())
f.message = false
f.callbacks.DecoderFilterCallbacks().SendLocalReply(httpStatus, recorder.Body.String(), recorder.Header(), 0, "")
return api.LocalReply
}
}
}
if f.serverName != "" {
if f.config.redisClient != nil {
// handle specific server
for _, server := range f.config.servers {
if f.serverName == server.GetServerName() {
buffer.Reset()
server.HandleSSE(f.callbacks, f.stopChan)
return api.Running
}
}
// handle default server
if f.serverName == f.config.defaultServer.GetServerName() {
buffer.Reset()
f.config.defaultServer.HandleSSE(f.callbacks, f.stopChan)
return api.Running
}
return api.Continue
} else {
buffer.SetString(RedisNotEnabledResponseBody)
return api.Continue
}
}
return api.Continue
}
// OnDestroy stops the goroutine
func (f *filter) OnDestroy(reason api.DestroyReason) {
api.LogDebugf("OnDestroy: reason=%v", reason)
if f.serverName != "" && f.stopChan != nil {
select {
case <-f.stopChan:
return
default:
api.LogDebug("Stopping SSE connection")
close(f.stopChan)
}
}
func (f *filter) EncodeHeaders(header api.ResponseHeaderMap, endStream bool) api.StatusType {
return api.Continue
}
func (f *filter) EncodeData(buffer api.BufferInstance, endStream bool) api.StatusType {
return api.Continue
}

View File

@@ -5,8 +5,8 @@ import (
"fmt"
"time"
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/registry"
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
"github.com/mark3labs/mcp-go/mcp"
"github.com/nacos-group/nacos-sdk-go/v2/clients"
@@ -15,7 +15,7 @@ import (
)
func init() {
internal.GlobalRegistry.RegisterServer("nacos-mcp-registry", &NacosConfig{})
common.GlobalRegistry.RegisterServer("nacos-mcp-registry", &NacosConfig{})
}
type NacosConfig struct {
@@ -28,7 +28,7 @@ type NacosConfig struct {
}
type McpServerToolsChangeListener struct {
mcpServer *internal.MCPServer
mcpServer *common.MCPServer
}
func (l *McpServerToolsChangeListener) OnToolChanged(reg registry.McpServerRegistry) {
@@ -137,8 +137,8 @@ func (c *NacosConfig) ParseConfig(config map[string]any) error {
return nil
}
func (c *NacosConfig) NewServer(serverName string) (*internal.MCPServer, error) {
mcpServer := internal.NewMCPServer(
func (c *NacosConfig) NewServer(serverName string) (*common.MCPServer, error) {
mcpServer := common.NewMCPServer(
serverName,
"1.0.0",
)
@@ -170,11 +170,11 @@ func (c *NacosConfig) NewServer(serverName string) (*internal.MCPServer, error)
return mcpServer, nil
}
func resetToolsToMcpServer(mcpServer *internal.MCPServer, reg registry.McpServerRegistry) {
wrappedTools := []internal.ServerTool{}
func resetToolsToMcpServer(mcpServer *common.MCPServer, reg registry.McpServerRegistry) {
wrappedTools := []common.ServerTool{}
tools := reg.ListToolsDesciption()
for _, tool := range tools {
wrappedTools = append(wrappedTools, internal.ServerTool{
wrappedTools = append(wrappedTools, common.ServerTool{
Tool: mcp.NewToolWithRawSchema(tool.Name, tool.Description, tool.InputSchema),
Handler: registry.HandleRegistryToolsCall(reg),
})

View File

@@ -9,7 +9,7 @@ import (
"net/url"
"strings"
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
"github.com/mark3labs/mcp-go/mcp"
)
@@ -204,7 +204,7 @@ func CommonRemoteCall(reg McpServerRegistry, toolName string, parameters map[str
return remoteHandle.HandleToolCall(ctx, parameters)
}
func HandleRegistryToolsCall(reg McpServerRegistry) internal.ToolHandlerFunc {
func HandleRegistryToolsCall(reg McpServerRegistry) common.ToolHandlerFunc {
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
arguments := request.Params.Arguments
return CommonRemoteCall(reg, request.Params.Name, arguments)

View File

@@ -1,47 +1,148 @@
package gorm
import (
"context"
"fmt"
"sync/atomic"
"time"
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
"gorm.io/driver/clickhouse"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// DBClient is a struct to handle PostgreSQL connections and operations
// DBClient is a struct to handle database connections and operations
type DBClient struct {
db *gorm.DB
db *gorm.DB
dsn string
dbType string
reconnect chan struct{}
stop chan struct{}
panicCount int32 // Add panic counter
}
// NewDBClient creates a new DBClient instance and establishes a connection to the PostgreSQL database
func NewDBClient(dsn string, dbType string) (*DBClient, error) {
var db *gorm.DB
var err error
if dbType == "postgres" {
db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
} else if dbType == "clickhouse" {
db, err = gorm.Open(clickhouse.Open(dsn), &gorm.Config{})
} else if dbType == "mysql" {
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
} else if dbType == "sqlite" {
db, err = gorm.Open(sqlite.Open(dsn), &gorm.Config{})
} else {
return nil, fmt.Errorf("unsupported database type %s", dbType)
}
// Connect to the database
if err != nil {
return nil, fmt.Errorf("failed to connect to database: %w", err)
// NewDBClient creates a new DBClient instance and establishes a connection to the database
func NewDBClient(dsn string, dbType string, stop chan struct{}) *DBClient {
client := &DBClient{
dsn: dsn,
dbType: dbType,
reconnect: make(chan struct{}, 1),
stop: stop,
}
return &DBClient{db: db}, nil
// Start reconnection goroutine
go client.reconnectLoop()
// Try initial connection
if err := client.connect(); err != nil {
api.LogErrorf("Initial database connection failed: %v", err)
}
return client
}
func (c *DBClient) connect() error {
var db *gorm.DB
var err error
gormConfig := gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
}
switch c.dbType {
case "postgres":
db, err = gorm.Open(postgres.Open(c.dsn), &gormConfig)
case "clickhouse":
db, err = gorm.Open(clickhouse.Open(c.dsn), &gormConfig)
case "mysql":
db, err = gorm.Open(mysql.Open(c.dsn), &gormConfig)
case "sqlite":
db, err = gorm.Open(sqlite.Open(c.dsn), &gormConfig)
default:
return fmt.Errorf("unsupported database type %s", c.dbType)
}
if err != nil {
return fmt.Errorf("failed to connect to database: %w", err)
}
c.db = db
return nil
}
func (c *DBClient) reconnectLoop() {
defer func() {
if r := recover(); r != nil {
api.LogErrorf("Recovered from panic in reconnectLoop: %v", r)
// Increment panic counter
atomic.AddInt32(&c.panicCount, 1)
// If panic count exceeds threshold, stop trying to reconnect
if atomic.LoadInt32(&c.panicCount) > 3 {
api.LogErrorf("Too many panics in reconnectLoop, stopping reconnection attempts")
return
}
// Wait for a while before restarting
time.Sleep(5 * time.Second)
// Restart the reconnect loop
go c.reconnectLoop()
}
}()
ticker := time.NewTicker(30 * time.Second) // Try to reconnect every 30 seconds
defer ticker.Stop()
for {
select {
case <-c.stop:
api.LogInfof("Database %s connection closed", c.dbType)
return
case <-ticker.C:
if c.db == nil || c.Ping() != nil {
if err := c.connect(); err != nil {
api.LogErrorf("Database reconnection failed: %v", err)
} else {
api.LogInfof("Database reconnected successfully")
// Reset panic count on successful connection
atomic.StoreInt32(&c.panicCount, 0)
}
}
case <-c.reconnect:
if err := c.connect(); err != nil {
api.LogErrorf("Database reconnection failed: %v", err)
} else {
api.LogInfof("Database reconnected successfully")
// Reset panic count on successful connection
atomic.StoreInt32(&c.panicCount, 0)
}
}
}
}
// ExecuteSQL executes a raw SQL query and returns the result as a slice of maps
func (c *DBClient) ExecuteSQL(query string, args ...interface{}) ([]map[string]interface{}, error) {
if c.db == nil {
// Trigger reconnection
select {
case c.reconnect <- struct{}{}:
default:
}
return nil, fmt.Errorf("database is not connected, attempting to reconnect")
}
rows, err := c.db.Raw(query, args...).Rows()
if err != nil {
// If execution fails, connection might be lost, trigger reconnection
select {
case c.reconnect <- struct{}{}:
default:
}
return nil, fmt.Errorf("failed to execute SQL query: %w", err)
}
defer rows.Close()
@@ -88,3 +189,21 @@ func (c *DBClient) ExecuteSQL(query string, args ...interface{}) ([]map[string]i
return results, nil
}
func (c *DBClient) Ping() error {
if c.db == nil {
return fmt.Errorf("database connection is nil")
}
// Use context to set timeout
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
// Try to ping the database
sqlDB, err := c.db.DB()
if err != nil {
return fmt.Errorf("failed to get underlying *sql.DB: %v", err)
}
return sqlDB.PingContext(ctx)
}

View File

@@ -4,7 +4,7 @@ import (
"errors"
"fmt"
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
"github.com/mark3labs/mcp-go/mcp"
)
@@ -12,12 +12,13 @@ import (
const Version = "1.0.0"
func init() {
internal.GlobalRegistry.RegisterServer("database", &DBConfig{})
common.GlobalRegistry.RegisterServer("database", &DBConfig{})
}
type DBConfig struct {
dbType string
dsn string
dbType string
dsn string
description string
}
func (c *DBConfig) ParseConfig(config map[string]any) error {
@@ -33,24 +34,24 @@ func (c *DBConfig) ParseConfig(config map[string]any) error {
}
c.dbType = dbType
api.LogDebugf("DBConfig ParseConfig: %+v", config)
c.description, ok = config["description"].(string)
if !ok {
c.description = ""
}
return nil
}
func (c *DBConfig) NewServer(serverName string) (*internal.MCPServer, error) {
mcpServer := internal.NewMCPServer(
func (c *DBConfig) NewServer(serverName string) (*common.MCPServer, error) {
mcpServer := common.NewMCPServer(
serverName,
Version,
internal.WithInstructions(fmt.Sprintf("This is a %s database server", c.dbType)),
common.WithInstructions(fmt.Sprintf("This is a %s database server", c.dbType)),
)
dbClient, err := NewDBClient(c.dsn, c.dbType)
if err != nil {
return nil, fmt.Errorf("failed to initialize DBClient: %w", err)
}
dbClient := NewDBClient(c.dsn, c.dbType, mcpServer.GetDestoryChannel())
// Add query tool
mcpServer.AddTool(
mcp.NewToolWithRawSchema("query", fmt.Sprintf("Run a read-only SQL query in database %s", c.dbType), GetQueryToolSchema()),
mcp.NewToolWithRawSchema("query", fmt.Sprintf("Run a read-only SQL query in database %s. Database description: %s", c.dbType, c.description), GetQueryToolSchema()),
HandleQueryTool(dbClient),
)

View File

@@ -5,12 +5,12 @@ import (
"encoding/json"
"fmt"
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
"github.com/mark3labs/mcp-go/mcp"
)
// HandleQueryTool handles SQL query execution
func HandleQueryTool(dbClient *DBClient) internal.ToolHandlerFunc {
func HandleQueryTool(dbClient *DBClient) common.ToolHandlerFunc {
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
arguments := request.Params.Arguments
message, ok := arguments["sql"].(string)

View File

@@ -1,4 +1,4 @@
package internal
package common
import (
"crypto/aes"

View File

@@ -1,4 +1,4 @@
package internal
package common
import (
"regexp"
@@ -23,6 +23,27 @@ type MatchRule struct {
MatchRuleType RuleType `json:"match_rule_type"` // Type of match rule
}
// ParseMatchList parses the match list from the config
func ParseMatchList(matchListConfig []interface{}) []MatchRule {
matchList := make([]MatchRule, 0)
for _, item := range matchListConfig {
if ruleMap, ok := item.(map[string]interface{}); ok {
rule := MatchRule{}
if domain, ok := ruleMap["match_rule_domain"].(string); ok {
rule.MatchRuleDomain = domain
}
if path, ok := ruleMap["match_rule_path"].(string); ok {
rule.MatchRulePath = path
}
if ruleType, ok := ruleMap["match_rule_type"].(string); ok {
rule.MatchRuleType = RuleType(ruleType)
}
matchList = append(matchList, rule)
}
}
return matchList
}
// convertWildcardToRegex converts wildcard pattern to regex pattern
func convertWildcardToRegex(pattern string) string {
pattern = regexp.QuoteMeta(pattern)
@@ -87,3 +108,13 @@ func IsMatch(rules []MatchRule, host, path string) bool {
}
return false
}
// MatchDomainList checks if the domain matches any of the domains in the list
func MatchDomainList(domain string, domainList []string) bool {
for _, d := range domainList {
if matchDomain(domain, d) {
return true
}
}
return false
}

View File

@@ -1,4 +1,4 @@
package internal
package common
import (
"context"
@@ -72,9 +72,10 @@ func NewRedisClient(config *RedisConfig) (*RedisClient, error) {
// Ping the Redis server to check the connection
pong, err := client.Ping(context.Background()).Result()
if err != nil {
return nil, fmt.Errorf("failed to connect to Redis: %w", err)
api.LogErrorf("Failed to connect to Redis: %v", err)
} else {
api.LogDebugf("Connected to Redis: %s", pong)
}
api.LogDebugf("Connected to Redis: %s", pong)
ctx, cancel := context.WithCancel(context.Background())
@@ -83,7 +84,7 @@ func NewRedisClient(config *RedisConfig) (*RedisClient, error) {
crypto, err = NewCrypto(config.secret)
if err != nil {
cancel()
return nil, err
api.LogWarnf("Failed to initialize redis crypto: %v", err)
}
}
@@ -103,7 +104,7 @@ func NewRedisClient(config *RedisConfig) (*RedisClient, error) {
// keepAlive periodically checks Redis connection and attempts to reconnect if needed
func (r *RedisClient) keepAlive() {
ticker := time.NewTicker(30 * time.Second)
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
@@ -249,6 +250,18 @@ func (r *RedisClient) Get(key string) (string, error) {
return value, nil
}
// Expire sets the expiration time for a key
func (r *RedisClient) Expire(key string, expiration time.Duration) error {
ok, err := r.client.Expire(r.ctx, key, expiration).Result()
if err != nil {
return fmt.Errorf("failed to set expiration for key: %w", err)
}
if !ok {
return fmt.Errorf("key does not exist")
}
return nil
}
// Close closes the Redis client and stops the keepalive goroutine
func (r *RedisClient) Close() error {
r.cancel()

View File

@@ -1,4 +1,4 @@
package internal
package common
var GlobalRegistry = NewServerRegistry()

View File

@@ -1,4 +1,4 @@
package internal
package common
import (
"context"
@@ -78,6 +78,7 @@ type MCPServer struct {
clientMu sync.Mutex // Separate mutex for client context
currentClient NotificationContext
initialized atomic.Bool // Use atomic for the initialized flag
destory chan struct{}
}
// serverKey is the context key for storing the server instance
@@ -226,6 +227,7 @@ func NewMCPServer(
prompts: nil,
logging: false,
},
destory: make(chan struct{}),
}
for _, opt := range opts {
@@ -241,6 +243,7 @@ func (s *MCPServer) HandleMessage(
message json.RawMessage,
) mcp.JSONRPCMessage {
// Add server to context
ctx = context.WithValue(ctx, serverKey{}, s)
var baseMessage struct {
@@ -826,6 +829,14 @@ func (s *MCPServer) handleNotification(
return nil
}
func (s *MCPServer) Close() {
close(s.destory)
}
func (s *MCPServer) GetDestoryChannel() chan struct{} {
return s.destory
}
func createResponse(id interface{}, result interface{}) mcp.JSONRPCMessage {
return mcp.JSONRPCResponse{
JSONRPC: mcp.JSONRPC_VERSION,

View File

@@ -1,4 +1,4 @@
package internal
package common
import (
"encoding/json"
@@ -179,10 +179,10 @@ func (s *SSEServer) HandleSSE(cb api.FilterCallbackHandler, stopChan chan struct
// handleMessage processes incoming JSON-RPC messages from clients and sends responses
// back through both the SSE connection and HTTP response.
func (s *SSEServer) HandleMessage(w http.ResponseWriter, r *http.Request, body json.RawMessage) {
func (s *SSEServer) HandleMessage(w http.ResponseWriter, r *http.Request, body json.RawMessage) int {
if r.Method != http.MethodPost {
s.writeJSONRPCError(w, nil, mcp.INVALID_REQUEST, fmt.Sprintf("Method %s not allowed", r.Method))
return
return http.StatusBadRequest
}
sessionID := r.URL.Query().Get("sessionId")
@@ -207,27 +207,26 @@ func (s *SSEServer) HandleMessage(w http.ResponseWriter, r *http.Request, body j
// Process message through MCPServer
response := s.server.HandleMessage(ctx, body)
var status int
// Only send response if there is one (not for notifications)
if response != nil {
eventData, _ := json.Marshal(response)
if sessionID != "" && s.redisClient != nil {
channel := GetSSEChannelName(sessionID)
publishErr := s.redisClient.Publish(channel, fmt.Sprintf("event: message\ndata: %s\n\n", eventData))
if publishErr != nil {
api.LogErrorf("Failed to publish message to Redis: %v", publishErr)
}
if sessionID != ""{
w.WriteHeader(http.StatusAccepted)
status = http.StatusAccepted
} else {
// support streamable http
w.WriteHeader(http.StatusOK)
status = http.StatusOK
}
// Send HTTP response
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusAccepted)
json.NewEncoder(w).Encode(response)
} else {
// For notifications, just send 202 Accepted with no body
w.WriteHeader(http.StatusAccepted)
status = http.StatusAccepted
}
return status
}
// writeJSONRPCError writes a JSON-RPC error response with the given error details.
@@ -242,3 +241,7 @@ func (s *SSEServer) writeJSONRPCError(
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(response)
}
func (s *SSEServer) Close() {
s.server.Close()
}

View File

@@ -0,0 +1,34 @@
package common
import (
"fmt"
"net/url"
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
)
type RequestURL struct {
Method string
Scheme string
Host string
Path string
BaseURL string
ParsedURL *url.URL
InternalIP bool
}
func NewRequestURL(header api.RequestHeaderMap) *RequestURL {
method, _ := header.Get(":method")
scheme, _ := header.Get(":scheme")
host, _ := header.Get(":authority")
path, _ := header.Get(":path")
internalIP, _ := header.Get("x-envoy-internal")
baseURL := fmt.Sprintf("%s://%s", scheme, host)
parsedURL, err := url.Parse(path)
if err != nil {
api.LogWarnf("url parse path:%s failed:%s", path, err)
return nil
}
api.LogDebugf("RequestURL: method=%s, scheme=%s, host=%s, path=%s", method, scheme, host, path)
return &RequestURL{Method: method, Scheme: scheme, Host: host, Path: path, BaseURL: baseURL, ParsedURL: parsedURL, InternalIP: internalIP == "true"}
}

View File

@@ -0,0 +1,145 @@
package mcp_session
import (
"fmt"
_ "net/http/pprof"
xds "github.com/cncf/xds/go/xds/type/v3"
"google.golang.org/protobuf/types/known/anypb"
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/handler"
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
)
const Name = "mcp-session"
const Version = "1.0.0"
const ConfigPathSuffix = "/config"
const DefaultServerName = "higress-mcp-server"
var GlobalSSEPathSuffix = "/sse"
type config struct {
matchList []common.MatchRule
enableUserLevelServer bool
rateLimitConfig *handler.MCPRatelimitConfig
defaultServer *common.SSEServer
redisClient *common.RedisClient
}
func (c *config) Destroy() {
if c.redisClient != nil {
api.LogDebug("Closing Redis client")
c.redisClient.Close()
}
}
type Parser struct {
}
// Parse the filter configuration
func (p *Parser) Parse(any *anypb.Any, callbacks api.ConfigCallbackHandler) (interface{}, error) {
configStruct := &xds.TypedStruct{}
if err := any.UnmarshalTo(configStruct); err != nil {
return nil, err
}
v := configStruct.Value
conf := &config{
matchList: make([]common.MatchRule, 0),
}
// Parse match_list if exists
if matchList, ok := v.AsMap()["match_list"].([]interface{}); ok {
conf.matchList = common.ParseMatchList(matchList)
}
// Redis configuration is optional
if redisConfigMap, ok := v.AsMap()["redis"].(map[string]interface{}); ok {
redisConfig, err := common.ParseRedisConfig(redisConfigMap)
if err != nil {
return nil, fmt.Errorf("failed to parse redis config: %w", err)
}
redisClient, err := common.NewRedisClient(redisConfig)
if err != nil {
api.LogErrorf("Failed to initialize Redis client: %w", err)
} else {
api.LogDebug("Redis client initialized")
}
conf.redisClient = redisClient
} else {
api.LogDebug("Redis configuration not provided, running without Redis")
}
enableUserLevelServer, ok := v.AsMap()["enable_user_level_server"].(bool)
if !ok {
enableUserLevelServer = false
if conf.redisClient == nil {
return nil, fmt.Errorf("redis configuration is not provided, enable_user_level_server is true")
}
}
conf.enableUserLevelServer = enableUserLevelServer
if rateLimit, ok := v.AsMap()["rate_limit"].(map[string]interface{}); ok {
rateLimitConfig := &handler.MCPRatelimitConfig{}
if limit, ok := rateLimit["limit"].(float64); ok {
rateLimitConfig.Limit = int(limit)
}
if window, ok := rateLimit["window"].(float64); ok {
rateLimitConfig.Window = int(window)
}
if whiteList, ok := rateLimit["white_list"].([]interface{}); ok {
for _, item := range whiteList {
if uid, ok := item.(string); ok {
rateLimitConfig.Whitelist = append(rateLimitConfig.Whitelist, uid)
}
}
}
if errorText, ok := rateLimit["error_text"].(string); ok {
rateLimitConfig.ErrorText = errorText
}
conf.rateLimitConfig = rateLimitConfig
}
ssePathSuffix, ok := v.AsMap()["sse_path_suffix"].(string)
if !ok || ssePathSuffix == "" {
return nil, fmt.Errorf("sse path suffix is not set or empty")
}
GlobalSSEPathSuffix = ssePathSuffix
return conf, nil
}
func (p *Parser) Merge(parent interface{}, child interface{}) interface{} {
parentConfig := parent.(*config)
childConfig := child.(*config)
newConfig := *parentConfig
if childConfig.matchList != nil {
newConfig.matchList = childConfig.matchList
}
newConfig.enableUserLevelServer = childConfig.enableUserLevelServer
if childConfig.rateLimitConfig != nil {
newConfig.rateLimitConfig = childConfig.rateLimitConfig
}
if childConfig.defaultServer != nil {
newConfig.defaultServer = childConfig.defaultServer
}
return &newConfig
}
func FilterFactory(c interface{}, callbacks api.FilterCallbackHandler) api.StreamFilter {
conf, ok := c.(*config)
if !ok {
panic("unexpected config type")
}
return &filter{
callbacks: callbacks,
config: conf,
stopChan: make(chan struct{}),
mcpConfigHandler: handler.NewMCPConfigHandler(conf.redisClient, callbacks),
mcpRatelimitHandler: handler.NewMCPRatelimitHandler(conf.redisClient, callbacks, conf.rateLimitConfig),
}
}

View File

@@ -0,0 +1,240 @@
package mcp_session
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/handler"
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
"github.com/mark3labs/mcp-go/mcp"
)
const (
RedisNotEnabledResponseBody = "Redis is not enabled, SSE connection is not supported"
)
// The callbacks in the filter, like `DecodeHeaders`, can be implemented on demand.
// Because api.PassThroughStreamFilter provides a default implementation.
type filter struct {
api.PassThroughStreamFilter
callbacks api.FilterCallbackHandler
path string
config *config
stopChan chan struct{}
req *http.Request
serverName string
proxyURL *url.URL
neepProcess bool
userLevelConfig bool
mcpConfigHandler *handler.MCPConfigHandler
ratelimit bool
mcpRatelimitHandler *handler.MCPRatelimitHandler
}
// 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 {
return api.Continue
}
f.path = url.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())
return api.Continue
}
f.neepProcess = true
f.req = &http.Request{
Method: url.Method,
URL: url.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())
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusForbidden, "", nil, 0, "")
return api.LocalReply
}
if strings.HasSuffix(f.path, ConfigPathSuffix) && url.Method == http.MethodGet {
api.LogDebugf("Handling config request: %s", f.path)
f.mcpConfigHandler.HandleConfigRequest(f.req, []byte{})
return api.LocalReply
}
f.userLevelConfig = true
if endStream {
return api.Continue
} else {
return api.StopAndBuffer
}
}
if !strings.HasSuffix(url.ParsedURL.Path, GlobalSSEPathSuffix) {
f.proxyURL = url.ParsedURL
if f.config.enableUserLevelServer {
parts := strings.Split(url.ParsedURL.Path, "/")
if len(parts) >= 3 {
serverName := parts[1]
uid := parts[2]
// Get encoded config
encodedConfig, _ := f.mcpConfigHandler.GetEncodedConfig(serverName, uid)
if encodedConfig != "" {
header.Set("x-higress-mcpserver-config", encodedConfig)
api.LogDebugf("Set x-higress-mcpserver-config Header for %s:%s", serverName, uid)
}
}
f.ratelimit = true
}
if endStream {
return api.Continue
} else {
return api.StopAndBuffer
}
}
if url.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.WithRedisClient(f.config.redisClient))
f.serverName = f.config.defaultServer.GetServerName()
body := "SSE connection create"
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusOK, body, nil, 0, "")
}
return api.LocalReply
}
// 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 {
return api.Continue
}
if !endStream {
return api.StopAndBuffer
}
if f.userLevelConfig {
// Handle config POST request
api.LogDebugf("Handling config request: %s", f.path)
f.mcpConfigHandler.HandleConfigRequest(f.req, buffer.Bytes())
return api.LocalReply
} else if f.ratelimit {
if checkJSONRPCMethod(buffer.Bytes(), "tools/list") {
api.LogDebugf("Not a tools call request, skipping ratelimit")
return api.Continue
}
parts := strings.Split(f.req.URL.Path, "/")
if len(parts) < 3 {
api.LogWarnf("Access denied: no valid uid found")
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusForbidden, "", nil, 0, "")
return api.LocalReply
}
serverName := parts[1]
uid := parts[2]
encodedConfig, err := f.mcpConfigHandler.GetEncodedConfig(serverName, uid)
if err != nil {
api.LogWarnf("Access denied: no valid config found for uid %s", uid)
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusForbidden, "", nil, 0, "")
return api.LocalReply
} else if encodedConfig == "" && checkJSONRPCMethod(buffer.Bytes(), "tools/call") {
api.LogDebugf("Empty config found for %s:%s", serverName, uid)
if !f.mcpRatelimitHandler.HandleRatelimit(f.req, buffer.Bytes()) {
return api.LocalReply
}
}
}
return api.Continue
}
// 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 {
return api.Continue
}
if f.serverName != "" {
if f.config.redisClient != nil {
header.Set("Content-Type", "text/event-stream")
header.Set("Cache-Control", "no-cache")
header.Set("Connection", "keep-alive")
header.Set("Access-Control-Allow-Origin", "*")
header.Del("Content-Length")
} else {
header.Set("Content-Length", strconv.Itoa(len(RedisNotEnabledResponseBody)))
}
return api.Continue
}
return api.Continue
}
// 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 {
return api.Continue
}
if !endStream {
return api.StopAndBuffer
}
if f.proxyURL != nil && f.config.redisClient != nil {
sessionID := f.proxyURL.Query().Get("sessionId")
if sessionID != "" {
channel := common.GetSSEChannelName(sessionID)
eventData := fmt.Sprintf("event: message\ndata: %s\n\n", buffer.String())
publishErr := f.config.redisClient.Publish(channel, eventData)
if publishErr != nil {
api.LogErrorf("Failed to publish wasm mcp server message to Redis: %v", publishErr)
}
}
}
if f.serverName != "" {
if f.config.redisClient != nil {
// handle default server
buffer.Reset()
f.config.defaultServer.HandleSSE(f.callbacks, f.stopChan)
return api.Running
} else {
buffer.SetString(RedisNotEnabledResponseBody)
return api.Continue
}
}
return api.Continue
}
// OnDestroy stops the goroutine
func (f *filter) OnDestroy(reason api.DestroyReason) {
api.LogDebugf("OnDestroy: reason=%v", reason)
if f.serverName != "" && f.stopChan != nil {
select {
case <-f.stopChan:
return
default:
api.LogDebug("Stopping SSE connection")
close(f.stopChan)
}
}
}
// check if the request is a tools/call request
func checkJSONRPCMethod(body []byte, method string) bool {
var request mcp.CallToolRequest
if err := json.Unmarshal(body, &request); err != nil {
api.LogWarnf("Failed to unmarshal request body: %v, not a JSON RPC request", err)
return true
}
return request.Method == method
}

View File

@@ -7,7 +7,7 @@ import (
"net/http"
"strings"
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
)
@@ -18,7 +18,7 @@ type MCPConfigHandler struct {
}
// NewMCPConfigHandler creates a new instance of MCP configuration handler
func NewMCPConfigHandler(redisClient *internal.RedisClient, callbacks api.FilterCallbackHandler) *MCPConfigHandler {
func NewMCPConfigHandler(redisClient *common.RedisClient, callbacks api.FilterCallbackHandler) *MCPConfigHandler {
return &MCPConfigHandler{
configStore: NewRedisConfigStore(redisClient),
callbacks: callbacks,
@@ -26,14 +26,14 @@ func NewMCPConfigHandler(redisClient *internal.RedisClient, callbacks api.Filter
}
// HandleConfigRequest processes configuration requests
func (h *MCPConfigHandler) HandleConfigRequest(path string, method string, body []byte) bool {
func (h *MCPConfigHandler) HandleConfigRequest(req *http.Request, body []byte) bool {
// Check if it's a configuration request
if !strings.HasSuffix(path, "/config") {
if !strings.HasSuffix(req.URL.Path, "/config") {
return false
}
// Extract serverName and uid from path
pathParts := strings.Split(strings.TrimSuffix(path, "/config"), "/")
pathParts := strings.Split(strings.TrimSuffix(req.URL.Path, "/config"), "/")
if len(pathParts) < 2 {
h.sendErrorResponse(http.StatusBadRequest, "INVALID_PATH", "Invalid path format")
return true
@@ -41,7 +41,7 @@ func (h *MCPConfigHandler) HandleConfigRequest(path string, method string, body
uid := pathParts[len(pathParts)-1]
serverName := pathParts[len(pathParts)-2]
switch method {
switch req.Method {
case http.MethodGet:
return h.handleGetConfig(serverName, uid)
case http.MethodPost:
@@ -70,10 +70,13 @@ func (h *MCPConfigHandler) handleGetConfig(serverName string, uid string) bool {
}
responseBytes, _ := json.Marshal(response)
headers := map[string][]string{
"Content-Type": {"application/json"},
}
h.callbacks.DecoderFilterCallbacks().SendLocalReply(
http.StatusOK,
string(responseBytes),
nil, 0, "",
headers, 0, "",
)
return true
}
@@ -103,10 +106,13 @@ func (h *MCPConfigHandler) handleStoreConfig(serverName string, uid string, body
}
responseBytes, _ := json.Marshal(response)
headers := map[string][]string{
"Content-Type": {"application/json"},
}
h.callbacks.DecoderFilterCallbacks().SendLocalReply(
http.StatusOK,
string(responseBytes),
nil, 0, "",
headers, 0, "",
)
return true
}
@@ -124,10 +130,13 @@ func (h *MCPConfigHandler) sendErrorResponse(status int, code string, message st
},
}
responseBytes, _ := json.Marshal(response)
headers := map[string][]string{
"Content-Type": {"application/json"},
}
h.callbacks.DecoderFilterCallbacks().SendLocalReply(
status,
string(responseBytes),
nil, 0, "",
headers, 0, "",
)
}

View File

@@ -5,7 +5,7 @@ import (
"fmt"
"time"
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
)
const (
@@ -36,11 +36,11 @@ type ConfigStore interface {
// RedisConfigStore implements configuration storage using Redis
type RedisConfigStore struct {
redisClient *internal.RedisClient
redisClient *common.RedisClient
}
// NewRedisConfigStore creates a new instance of Redis configuration storage
func NewRedisConfigStore(redisClient *internal.RedisClient) ConfigStore {
func NewRedisConfigStore(redisClient *common.RedisClient) ConfigStore {
return &RedisConfigStore{
redisClient: redisClient,
}
@@ -101,5 +101,11 @@ func (s *RedisConfigStore) GetConfig(serverName string, uid string) (map[string]
return nil, err
}
// Refresh TTL
if err := s.redisClient.Expire(key, configExpiry); err != nil {
// Log error but don't fail the request
fmt.Printf("Failed to refresh TTL for key %s: %v\n", key, err)
}
return config, nil
}

View File

@@ -1,22 +1,25 @@
package handler
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
"github.com/mark3labs/mcp-go/mcp"
)
type MCPRatelimitHandler struct {
redisClient *internal.RedisClient
redisClient *common.RedisClient
callbacks api.FilterCallbackHandler
limit int // Maximum requests allowed per window
window int // Time window in seconds
whitelist []string // Whitelist of UIDs that bypass rate limiting
errorText string // Error text to be displayed
}
// MCPRatelimitConfig is the configuration for the rate limit handler
@@ -24,15 +27,17 @@ type MCPRatelimitConfig struct {
Limit int `json:"limit"`
Window int `json:"window"`
Whitelist []string `json:"white_list"` // List of UIDs that bypass rate limiting
ErrorText string `json:"error_text"` // Error text to be displayed
}
// NewMCPRatelimitHandler creates a new rate limit handler
func NewMCPRatelimitHandler(redisClient *internal.RedisClient, callbacks api.FilterCallbackHandler, conf *MCPRatelimitConfig) *MCPRatelimitHandler {
func NewMCPRatelimitHandler(redisClient *common.RedisClient, callbacks api.FilterCallbackHandler, conf *MCPRatelimitConfig) *MCPRatelimitHandler {
if conf == nil {
conf = &MCPRatelimitConfig{
Limit: 100,
Window: int(24 * time.Hour / time.Second), // 24 hours in seconds
Whitelist: []string{},
ErrorText: "API rate limit exceeded",
}
}
return &MCPRatelimitHandler{
@@ -41,6 +46,7 @@ func NewMCPRatelimitHandler(redisClient *internal.RedisClient, callbacks api.Fil
limit: conf.Limit,
window: conf.Window,
whitelist: conf.Whitelist,
errorText: conf.ErrorText,
}
}
@@ -62,8 +68,9 @@ type LimitContext struct {
Reset int // Time until reset in seconds
}
func (h *MCPRatelimitHandler) HandleRatelimit(path string, method string, body []byte) bool {
parts := strings.Split(path, "/")
// TODO: needs to be refactored, rate limit should be registered as a request hook in MCP server
func (h *MCPRatelimitHandler) HandleRatelimit(req *http.Request, body []byte) bool {
parts := strings.Split(req.URL.Path, "/")
if len(parts) < 3 {
h.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusForbidden, "", nil, 0, "")
return false
@@ -106,13 +113,58 @@ func (h *MCPRatelimitHandler) HandleRatelimit(path string, method string, body [
}
if context.Remaining < 0 {
h.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusTooManyRequests, "", nil, 0, "")
// Create error response content
errorContent := []mcp.TextContent{
{
Type: "text",
Text: h.errorText,
},
}
// Create response result
result := map[string]interface{}{
"content": errorContent,
"isError": true,
}
// Create JSON-RPC response
id := getJSONPRCID(body)
response := mcp.JSONRPCResponse{
JSONRPC: mcp.JSONRPC_VERSION,
ID: id,
Result: result,
}
// Convert response to JSON
jsonResponse, err := json.Marshal(response)
if err != nil {
api.LogErrorf("Failed to marshal JSON response: %v", err)
h.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusInternalServerError, "", nil, 0, "")
return false
}
// Send JSON-RPC response
sessionID := req.URL.Query().Get("sessionId")
if sessionID != "" {
h.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusAccepted, string(jsonResponse), nil, 0, "")
} else {
h.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusOK, string(jsonResponse), nil, 0, "")
}
return false
}
return true
}
func getJSONPRCID(body []byte) mcp.RequestId {
baseMessage := struct {
JSONRPC string `json:"jsonrpc"`
Method string `json:"method"`
ID interface{} `json:"id,omitempty"`
}{}
if err := json.Unmarshal(body, &baseMessage); err != nil {
api.LogWarnf("Failed to unmarshal request body: %v, not a JSON RPC request", err)
return ""
}
return baseMessage.ID
}
// parseRedisValue converts the value from Redis to an int
func parseRedisValue(value interface{}) int {
switch v := value.(type) {

View File

@@ -55,6 +55,7 @@ constexpr std::string_view Host(":authority");
constexpr std::string_view Path(":path");
constexpr std::string_view EnvoyOriginalPath("x-envoy-original-path");
constexpr std::string_view Accept("accept");
constexpr std::string_view ContentDisposition("content-disposition");
constexpr std::string_view ContentMD5("content-md5");
constexpr std::string_view ContentType("content-type");
constexpr std::string_view ContentLength("content-length");
@@ -68,6 +69,7 @@ constexpr std::string_view StrictTransportSecurity("strict-transport-security");
namespace ContentTypeValues {
constexpr std::string_view Grpc{"application/grpc"};
constexpr std::string_view Json{"application/json"};
constexpr std::string_view MultipartFormData{"multipart/form-data"};
} // namespace ContentTypeValues
class PercentEncoding {

View File

@@ -16,6 +16,7 @@
#include <array>
#include <limits>
#include <regex>
#include "absl/strings/str_cat.h"
#include "absl/strings/str_split.h"
@@ -123,6 +124,7 @@ bool PluginRootContext::configure(size_t configuration_size) {
}
FilterHeadersStatus PluginRootContext::onHeader(
PluginContext& ctx,
const ModelRouterConfigRule& rule) {
if (!Wasm::Common::Http::hasRequestBody()) {
return FilterHeadersStatus::Continue;
@@ -150,19 +152,49 @@ FilterHeadersStatus PluginRootContext::onHeader(
if (!enable) {
return FilterHeadersStatus::Continue;
}
auto content_type_value =
auto content_type_ptr =
getRequestHeader(Wasm::Common::Http::Header::ContentType);
if (!absl::StrContains(content_type_value->view(),
auto content_type_value = content_type_ptr->view();
LOG_DEBUG(absl::StrCat("Content-Type: ", content_type_value));
if (absl::StrContains(content_type_value,
Wasm::Common::Http::ContentTypeValues::Json)) {
return FilterHeadersStatus::Continue;
ctx.mode_ = MODE_JSON;
LOG_DEBUG("Enable JSON mode.");
removeRequestHeader(Wasm::Common::Http::Header::ContentLength);
setFilterState(SetDecoderBufferLimitKey, DefaultMaxBodyBytes);
LOG_INFO(absl::StrCat("SetRequestBodyBufferLimit: ", DefaultMaxBodyBytes));
return FilterHeadersStatus::StopIteration;
}
removeRequestHeader(Wasm::Common::Http::Header::ContentLength);
setFilterState(SetDecoderBufferLimitKey, DefaultMaxBodyBytes);
LOG_INFO(absl::StrCat("SetRequestBodyBufferLimit: ", DefaultMaxBodyBytes));
return FilterHeadersStatus::StopIteration;
if (absl::StrContains(content_type_value,
Wasm::Common::Http::ContentTypeValues::MultipartFormData)) {
// Get the boundary from the content type
auto boundary_start = content_type_value.find("boundary=");
if (boundary_start == std::string::npos) {
LOG_WARN(absl::StrCat("No boundary found in a multipart/form-data content-type: ", content_type_value));
return FilterHeadersStatus::Continue;
}
boundary_start += 9;
auto boundary_end = content_type_value.find(';', boundary_start);
if (boundary_end == std::string::npos) {
boundary_end = content_type_value.size();
}
auto boundary_length = boundary_end - boundary_start;
if (boundary_length < 1 || boundary_length > 70) {
// See https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html
LOG_WARN(absl::StrCat("Invalid boundary value in a multipart/form-data content-type: ", content_type_value));
return FilterHeadersStatus::Continue;
}
auto boundary_value = content_type_value.substr(boundary_start, boundary_end - boundary_start);
ctx.mode_ = MODE_MULTIPART;
ctx.boundary_ = boundary_value;
LOG_DEBUG(absl::StrCat("Enable multipart/form-data mode. Boundary=", boundary_value));
removeRequestHeader(Wasm::Common::Http::Header::ContentLength);
return FilterHeadersStatus::StopIteration;
}
return FilterHeadersStatus::Continue;
}
FilterDataStatus PluginRootContext::onBody(const ModelRouterConfigRule& rule,
FilterDataStatus PluginRootContext::onJsonBody(const ModelRouterConfigRule& rule,
std::string_view body) {
const auto& model_key = rule.model_key_;
const auto& add_provider_header = rule.add_provider_header_;
@@ -198,10 +230,85 @@ FilterDataStatus PluginRootContext::onBody(const ModelRouterConfigRule& rule,
return FilterDataStatus::Continue;
}
FilterDataStatus PluginRootContext::onMultipartBody(
PluginContext& ctx,
const ModelRouterConfigRule& rule,
WasmDataPtr& body,
bool end_stream) {
const auto& add_provider_header = rule.add_provider_header_;
const auto& model_to_header = rule.model_to_header_;
const auto boundary = ctx.boundary_;
const auto body_view = body->view();
const auto model_param_header = absl::StrCat("Content-Disposition: form-data; name=\"", rule.model_key_, "\"");
for (size_t pos = 0; (pos = body_view.find(boundary, pos)) != std::string_view::npos;) {
LOG_DEBUG(absl::StrCat("Found boundary at ", pos));
pos += boundary.length();
size_t end_pos = body_view.find(boundary, pos);
if (end_pos == std::string_view::npos) {
end_pos = body_view.length();
}
std::string_view part = body_view.substr(pos, end_pos - pos);
LOG_DEBUG(absl::StrCat("Part: ", part));
auto part_pos = pos;
pos = end_pos;
// Check if this part contains the model parameter
if (!absl::StrContains(part, model_param_header)) {
LOG_DEBUG("Part does not contain model parameter");
continue;
}
size_t value_start = part.find(CRLF_CRLF);
if (value_start == std::string_view::npos) {
LOG_DEBUG("No value start found in part");
break;
}
value_start += 4; // Skip the "\r\n\r\n"
// model parameter should be only one line
size_t value_end = part.find(CRLF, value_start);
if (value_end == std::string_view::npos) {
LOG_DEBUG("No value end found in part");
break;
}
auto model_value = part.substr(value_start, value_end - value_start);
LOG_DEBUG(absl::StrCat("Model value: ", model_value));
if (!model_to_header.empty()) {
replaceRequestHeader(model_to_header, model_value);
}
if (!add_provider_header.empty()) {
auto pos = model_value.find('/');
if (pos != std::string::npos) {
const auto& provider = model_value.substr(0, pos);
const auto& model = model_value.substr(pos + 1);
replaceRequestHeader(add_provider_header, provider);
size_t new_size = 0;
auto new_buffer_data = absl::StrCat(body_view.substr(0, part_pos + value_start), model, body_view.substr(part_pos + value_end));
auto result = setBuffer(WasmBufferType::HttpRequestBody, 0, std::numeric_limits<size_t>::max(), new_buffer_data, &new_size);
LOG_DEBUG(absl::StrCat("model route to provider:", provider,
", model:", model));
LOG_DEBUG(absl::StrCat("result=", result, " new_size=", new_size));
} else {
LOG_DEBUG(absl::StrCat("model route to provider not work, model:",
model_value));
}
}
// We are done now. We can stop processing the body.
LOG_DEBUG(absl::StrCat("Done processing multipart body after caching ", body_view.length() , " bytes."));
ctx.mode_ = MODE_BYPASS;
return FilterDataStatus::Continue;
}
if (end_stream) {
LOG_DEBUG("No model parameter found in the body");
return FilterDataStatus::Continue;
}
return FilterDataStatus::StopIterationAndBuffer;
}
FilterHeadersStatus PluginContext::onRequestHeaders(uint32_t, bool) {
auto* rootCtx = rootContext();
return rootCtx->onHeaders([rootCtx, this](const auto& config) {
auto ret = rootCtx->onHeader(config);
auto ret = rootCtx->onHeader(*this, config);
if (ret == FilterHeadersStatus::StopIteration) {
this->config_ = &config;
}
@@ -214,14 +321,28 @@ FilterDataStatus PluginContext::onRequestBody(size_t body_size,
if (config_ == nullptr) {
return FilterDataStatus::Continue;
}
body_total_size_ += body_size;
if (!end_stream) {
return FilterDataStatus::StopIterationAndBuffer;
}
auto body =
getBufferBytes(WasmBufferType::HttpRequestBody, 0, body_total_size_);
auto* rootCtx = rootContext();
return rootCtx->onBody(*config_, body->view());
body_total_size_ += body_size;
switch (mode_) {
case MODE_JSON:
{
if (!end_stream) {
return FilterDataStatus::StopIterationAndBuffer;
}
auto body =
getBufferBytes(WasmBufferType::HttpRequestBody, 0, body_total_size_);
return rootCtx->onJsonBody(*config_, body->view());
}
case MODE_MULTIPART:
{
auto body =
getBufferBytes(WasmBufferType::HttpRequestBody, 0, body_total_size_);
return rootCtx->onMultipartBody(*this, *config_, body, end_stream);
}
case MODE_BYPASS:
default:
return FilterDataStatus::Continue;
}
}
#ifdef NULL_PLUGIN

View File

@@ -36,6 +36,13 @@ namespace model_router {
#endif
#define MODE_BYPASS 0
#define MODE_JSON 1
#define MODE_MULTIPART 2
#define CRLF ("\r\n")
#define CRLF_CRLF ("\r\n\r\n")
struct ModelRouterConfigRule {
std::string model_key_ = "model";
std::string add_provider_header_;
@@ -45,6 +52,8 @@ struct ModelRouterConfigRule {
"/audio/speech", "/fine_tuning/jobs", "/moderations"};
};
class PluginContext;
// PluginRootContext is the root context for all streams processed by the
// thread. It has the same lifetime as the worker thread and acts as target for
// interactions that outlives individual stream, e.g. timer, async calls.
@@ -55,8 +64,9 @@ class PluginRootContext : public RootContext,
: RootContext(id, root_id) {}
~PluginRootContext() {}
bool onConfigure(size_t) override;
FilterHeadersStatus onHeader(const ModelRouterConfigRule&);
FilterDataStatus onBody(const ModelRouterConfigRule&, std::string_view);
FilterHeadersStatus onHeader(PluginContext& ctx, const ModelRouterConfigRule&);
FilterDataStatus onJsonBody(const ModelRouterConfigRule&, std::string_view);
FilterDataStatus onMultipartBody(PluginContext& ctx, const ModelRouterConfigRule& rule, WasmDataPtr& body, bool end_stream);
bool configure(size_t);
private:
@@ -69,6 +79,8 @@ class PluginContext : public Context {
explicit PluginContext(uint32_t id, RootContext* root) : Context(id, root) {}
FilterHeadersStatus onRequestHeaders(uint32_t, bool) override;
FilterDataStatus onRequestBody(size_t, bool) override;
int mode_;
std::string boundary_;
private:
inline PluginRootContext* rootContext() {

View File

@@ -15,6 +15,7 @@
#include "extensions/model_router/plugin.h"
#include <cstddef>
#include <regex>
#include "gmock/gmock.h"
#include "gtest/gtest.h"
@@ -86,7 +87,7 @@ class ModelRouterTest : public ::testing::Test {
.WillByDefault([&](WasmHeaderMapType, std::string_view header,
std::string_view* result) {
if (header == "content-type") {
*result = "application/json";
*result = content_type_;
} else if (header == "content-length") {
*result = "1024";
} else if (header == ":path") {
@@ -125,6 +126,7 @@ class ModelRouterTest : public ::testing::Test {
std::unique_ptr<PluginContext> context_;
std::string route_name_;
std::string path_;
std::string content_type_ = "application/json";
BufferBase body_;
BufferBase config_;
};
@@ -133,7 +135,7 @@ TEST_F(ModelRouterTest, RewriteModelAndHeader) {
std::string configuration = R"(
{
"addProviderHeader": "x-higress-llm-provider"
})";
})";
config_.set(configuration);
EXPECT_TRUE(root_context_->configure(configuration.size()));
@@ -155,14 +157,14 @@ TEST_F(ModelRouterTest, RewriteModelAndHeader) {
body_.set(request_json);
EXPECT_EQ(context_->onRequestHeaders(0, false),
FilterHeadersStatus::StopIteration);
EXPECT_EQ(context_->onRequestBody(28, true), FilterDataStatus::Continue);
EXPECT_EQ(context_->onRequestBody(request_json.length(), true), FilterDataStatus::Continue);
}
TEST_F(ModelRouterTest, ModelToHeader) {
std::string configuration = R"(
{
"modelToHeader": "x-higress-llm-model"
})";
})";
config_.set(configuration);
EXPECT_TRUE(root_context_->configure(configuration.size()));
@@ -181,14 +183,14 @@ TEST_F(ModelRouterTest, ModelToHeader) {
body_.set(request_json);
EXPECT_EQ(context_->onRequestHeaders(0, false),
FilterHeadersStatus::StopIteration);
EXPECT_EQ(context_->onRequestBody(28, true), FilterDataStatus::Continue);
EXPECT_EQ(context_->onRequestBody(request_json.length(), true), FilterDataStatus::Continue);
}
TEST_F(ModelRouterTest, IgnorePath) {
std::string configuration = R"(
{
"addProviderHeader": "x-higress-llm-provider"
})";
})";
config_.set(configuration);
EXPECT_TRUE(root_context_->configure(configuration.size()));
@@ -208,7 +210,7 @@ TEST_F(ModelRouterTest, IgnorePath) {
body_.set(request_json);
EXPECT_EQ(context_->onRequestHeaders(0, false),
FilterHeadersStatus::Continue);
EXPECT_EQ(context_->onRequestBody(28, true), FilterDataStatus::Continue);
EXPECT_EQ(context_->onRequestBody(request_json.length(), true), FilterDataStatus::Continue);
}
TEST_F(ModelRouterTest, RouteLevelRewriteModelAndHeader) {
@@ -242,7 +244,178 @@ TEST_F(ModelRouterTest, RouteLevelRewriteModelAndHeader) {
route_name_ = "route-a";
EXPECT_EQ(context_->onRequestHeaders(0, false),
FilterHeadersStatus::StopIteration);
EXPECT_EQ(context_->onRequestBody(28, true), FilterDataStatus::Continue);
EXPECT_EQ(context_->onRequestBody(request_json.length(), true), FilterDataStatus::Continue);
}
TEST_F(ModelRouterTest, RewriteModelAndHeaderMultipartFormData) {
std::string configuration = R"({
"addProviderHeader": "x-higress-llm-provider"
})";
config_.set(configuration);
EXPECT_TRUE(root_context_->configure(configuration.size()));
path_ = "/v1/chat/completions";
content_type_ = "multipart/form-data; boundary=--------------------------100751621174704322650451";
std::string request_data = std::regex_replace(R"(
----------------------------100751621174704322650451
Content-Disposition: form-data; name="purpose"
batch
----------------------------100751621174704322650451
Content-Disposition: form-data; name="model"
qwen/qwen-turbo
----------------------------100751621174704322650451
Content-Disposition: form-data; name="file"; filename="test-data.json"
Content-Type: application/json
[
]
----------------------------100751621174704322650451--
)", std::regex("\n"), "\r\n"); // Multipart data requires CRLF line endings
EXPECT_CALL(*mock_context_,
setBuffer(testing::_, testing::_, testing::_, testing::_))
.WillOnce([&](WasmBufferType, size_t start, size_t length, std::string_view body) {
std::cerr << "===============" << "\n";
std::cerr << body << "\n";
std::cerr << "===============" << "\n";
EXPECT_EQ(start, 0);
EXPECT_EQ(length, std::numeric_limits<size_t>::max());
auto expected_body= std::regex_replace(R"(
----------------------------100751621174704322650451
Content-Disposition: form-data; name="purpose"
batch
----------------------------100751621174704322650451
Content-Disposition: form-data; name="model"
qwen-turbo
)", std::regex("\n"), "\r\n"); // Multipart data requires CRLF line endings
EXPECT_EQ(body, expected_body);
return WasmResult::Ok;
});
EXPECT_CALL(*mock_context_,
replaceHeaderMapValue(testing::_,
std::string_view("x-higress-llm-provider"),
std::string_view("qwen")));
body_.set(request_data);
EXPECT_EQ(context_->onRequestHeaders(0, false),
FilterHeadersStatus::StopIteration);
auto last_body_size = 0;
auto body = request_data.substr(0, request_data.find("batch") + 5 + 2 /* batch + CRLF */);
body_.set(body);
EXPECT_EQ(context_->onRequestBody(body.size() - last_body_size, false), FilterDataStatus::StopIterationAndBuffer);
last_body_size = body.size();
body = request_data.substr(0, request_data.find("\"model\"") + 5 + 2 + 2 /* "model" + CRLF + CRLF */);
body_.set(body);
EXPECT_EQ(context_->onRequestBody(body.size() - last_body_size, false), FilterDataStatus::StopIterationAndBuffer);
last_body_size = body.size();
body = request_data.substr(0, request_data.find("qwen") + 4 /* "qwen" */);
body_.set(body);
EXPECT_EQ(context_->onRequestBody(body.size() - last_body_size, false), FilterDataStatus::StopIterationAndBuffer);
last_body_size = body.size();
body = request_data.substr(0, request_data.find("qwen-turbo") + 10 /* "qwen-turbo" */);
body_.set(body);
EXPECT_EQ(context_->onRequestBody(body.size() - last_body_size, false), FilterDataStatus::StopIterationAndBuffer);
last_body_size = body.size();
body = request_data.substr(0, request_data.find("qwen-turbo") + 10 + 2 /* "qwen-turbo" + CRLF */);
body_.set(body);
EXPECT_EQ(context_->onRequestBody(body.size() - last_body_size, false), FilterDataStatus::Continue);
last_body_size = body.size();
body = request_data.substr(0, request_data.find("qwen-turbo") + 10 + 2 + 50 /* "qwen-turbo" + CRLF + boundary */);
body_.set(body);
EXPECT_EQ(context_->onRequestBody(body.size() - last_body_size, false), FilterDataStatus::Continue);
last_body_size = body.size();
body_.set(request_data);
EXPECT_EQ(context_->onRequestBody(body.size() - last_body_size, true), FilterDataStatus::Continue);
}
TEST_F(ModelRouterTest, ModelToHeaderMultipartFormData) {
std::string configuration = R"(
{
"modelToHeader": "x-higress-llm-model"
})";
config_.set(configuration);
EXPECT_TRUE(root_context_->configure(configuration.size()));
path_ = "/v1/chat/completions";
content_type_ = "multipart/form-data; boundary=--------------------------100751621174704322650451";
std::string request_data = std::regex_replace(R"(
----------------------------100751621174704322650451
Content-Disposition: form-data; name="purpose"
batch
----------------------------100751621174704322650451
Content-Disposition: form-data; name="model"
qwen-max
----------------------------100751621174704322650451
Content-Disposition: form-data; name="file"; filename="test-data.json"
Content-Type: application/json
[
]
----------------------------100751621174704322650451--
)", std::regex("\n"), "\r\n"); // Multipart data requires CRLF line endings
EXPECT_CALL(*mock_context_,
setBuffer(testing::_, testing::_, testing::_, testing::_))
.Times(0);
EXPECT_CALL(
*mock_context_,
replaceHeaderMapValue(testing::_, std::string_view("x-higress-llm-model"),
std::string_view("qwen-max")));
EXPECT_EQ(context_->onRequestHeaders(0, false),
FilterHeadersStatus::StopIteration);
auto last_body_size = 0;
auto body = request_data.substr(0, request_data.find("batch") + 5 + 2 /* batch + CRLF */);
body_.set(body);
EXPECT_EQ(context_->onRequestBody(body.size() - last_body_size, false), FilterDataStatus::StopIterationAndBuffer);
last_body_size = body.size();
body = request_data.substr(0, request_data.find("\"model\"") + 5 + 2 + 2 /* "model" + CRLF + CRLF */);
body_.set(body);
EXPECT_EQ(context_->onRequestBody(body.size() - last_body_size, false), FilterDataStatus::StopIterationAndBuffer);
last_body_size = body.size();
body = request_data.substr(0, request_data.find("qwen") + 4 /* "qwen" */);
body_.set(body);
EXPECT_EQ(context_->onRequestBody(body.size() - last_body_size, false), FilterDataStatus::StopIterationAndBuffer);
last_body_size = body.size();
body = request_data.substr(0, request_data.find("qwen-max") + 8 /* "qwen-max" */);
body_.set(body);
EXPECT_EQ(context_->onRequestBody(body.size() - last_body_size, false), FilterDataStatus::StopIterationAndBuffer);
last_body_size = body.size();
body = request_data.substr(0, request_data.find("qwen-max") + 8 + 2 /* "qwen-max" + CRLF */);
body_.set(body);
EXPECT_EQ(context_->onRequestBody(body.size() - last_body_size, false), FilterDataStatus::Continue);
last_body_size = body.size();
body = request_data.substr(0, request_data.find("qwen-max") + 8 + 2 + 50 /* "qwen-max" + CRLF */);
body_.set(body);
EXPECT_EQ(context_->onRequestBody(body.size() - last_body_size, false), FilterDataStatus::Continue);
last_body_size = body.size();
body_.set(request_data);
EXPECT_EQ(context_->onRequestBody(body.size() - last_body_size, true), FilterDataStatus::Continue);
}
} // namespace model_router

View File

@@ -17,7 +17,14 @@ COPY . .
WORKDIR /workspace/extensions/$PLUGIN_NAME
RUN go mod tidy
RUN tinygo build -o /main.wasm -scheduler=none -gc=custom -tags="custommalloc nottinygc_finalizer $EXTRA_TAGS" -target=wasi ./
RUN \
if echo "$PLUGIN_NAME" | grep -Eq '^waf$'; then \
# Please use higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/wasm-go-builder:go1.19-tinygo0.28.1-oras1.0.0 as BUILDER
go run mage.go build && \
mv ./local/main.wasm /main.wasm ; \
else \
tinygo build -o /main.wasm -scheduler=none -gc=custom -tags="custommalloc nottinygc_finalizer $EXTRA_TAGS" -target=wasi ./ ; \
fi
FROM scratch as output

View File

@@ -0,0 +1,817 @@
package provider
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"hash"
"hash/crc32"
"io"
"net/http"
"strings"
"time"
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-proxy/util"
"github.com/alibaba/higress/plugins/wasm-go/pkg/log"
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
)
const (
httpPostMethod = "POST"
awsService = "bedrock"
// bedrock-runtime.{awsRegion}.amazonaws.com
bedrockDefaultDomain = "bedrock-runtime.%s.amazonaws.com"
// converse路径 /model/{modelId}/converse
bedrockChatCompletionPath = "/model/%s/converse"
// converseStream路径 /model/{modelId}/converse-stream
bedrockStreamChatCompletionPath = "/model/%s/converse-stream"
bedrockSignedHeaders = "host;x-amz-date"
)
type bedrockProviderInitializer struct {
}
func (b *bedrockProviderInitializer) ValidateConfig(config *ProviderConfig) error {
if len(config.awsAccessKey) == 0 || len(config.awsSecretKey) == 0 {
return errors.New("missing bedrock access authentication parameters")
}
if len(config.awsRegion) == 0 {
return errors.New("missing bedrock region parameters")
}
return nil
}
func (b *bedrockProviderInitializer) DefaultCapabilities() map[string]string {
return map[string]string{
string(ApiNameChatCompletion): bedrockChatCompletionPath,
}
}
func (b *bedrockProviderInitializer) CreateProvider(config ProviderConfig) (Provider, error) {
config.setDefaultCapabilities(b.DefaultCapabilities())
return &bedrockProvider{
config: config,
contextCache: createContextCache(&config),
}, nil
}
type bedrockProvider struct {
config ProviderConfig
contextCache *contextCache
}
func (b *bedrockProvider) OnStreamingResponseBody(ctx wrapper.HttpContext, name ApiName, chunk []byte, isLastChunk bool) ([]byte, error) {
events := extractAmazonEventStreamEvents(ctx, chunk)
if len(events) == 0 {
return chunk, fmt.Errorf("No events are extracted ")
}
var responseBuilder strings.Builder
for _, event := range events {
outputEvent, err := b.convertEventFromBedrockToOpenAI(ctx, event)
if err != nil {
log.Errorf("[onStreamingResponseBody] failed to process streaming event: %v\n%s", err, chunk)
return chunk, err
}
responseBuilder.WriteString(string(outputEvent))
}
return []byte(responseBuilder.String()), nil
}
func (b *bedrockProvider) convertEventFromBedrockToOpenAI(ctx wrapper.HttpContext, bedrockEvent ConverseStreamEvent) ([]byte, error) {
choices := make([]chatCompletionChoice, 0)
chatChoice := chatCompletionChoice{
Delta: &chatMessage{},
}
if bedrockEvent.Role != nil {
chatChoice.Delta.Role = *bedrockEvent.Role
}
if bedrockEvent.Delta != nil {
chatChoice.Delta = &chatMessage{Content: bedrockEvent.Delta.Text}
}
if bedrockEvent.StopReason != nil {
chatChoice.FinishReason = stopReasonBedrock2OpenAI(*bedrockEvent.StopReason)
}
choices = append(choices, chatChoice)
requestId := ctx.GetStringContext("X-Amzn-Requestid", "")
openAIFormattedChunk := &chatCompletionResponse{
Id: requestId,
Created: time.Now().UnixMilli() / 1000,
Model: ctx.GetStringContext(ctxKeyFinalRequestModel, ""),
SystemFingerprint: "",
Object: objectChatCompletion,
Choices: choices,
}
if bedrockEvent.Usage != nil {
openAIFormattedChunk.Choices = choices[:0]
openAIFormattedChunk.Usage = usage{
CompletionTokens: bedrockEvent.Usage.OutputTokens,
PromptTokens: bedrockEvent.Usage.InputTokens,
TotalTokens: bedrockEvent.Usage.TotalTokens,
}
}
openAIFormattedChunkBytes, _ := json.Marshal(openAIFormattedChunk)
var openAIChunk strings.Builder
openAIChunk.WriteString(ssePrefix)
openAIChunk.WriteString(string(openAIFormattedChunkBytes))
openAIChunk.WriteString("\n\n")
return []byte(openAIChunk.String()), nil
}
type ConverseStreamEvent struct {
ContentBlockIndex int `json:"contentBlockIndex,omitempty"`
Delta *converseStreamEventContentBlockDelta `json:"delta,omitempty"`
Role *string `json:"role,omitempty"`
StopReason *string `json:"stopReason,omitempty"`
Usage *tokenUsage `json:"usage,omitempty"`
Start *contentBlockStart `json:"start,omitempty"`
}
type converseStreamEventContentBlockDelta struct {
Text *string `json:"text,omitempty"`
ToolUse *toolUseBlockDelta `json:"toolUse,omitempty"`
}
type toolUseBlockStart struct {
Name string `json:"name"`
ToolUseID string `json:"toolUseId"`
}
type contentBlockStart struct {
ToolUse *toolUseBlockStart `json:"toolUse,omitempty"`
}
type toolUseBlockDelta struct {
Input string `json:"input"`
}
func extractAmazonEventStreamEvents(ctx wrapper.HttpContext, chunk []byte) []ConverseStreamEvent {
body := chunk
if bufferedStreamingBody, has := ctx.GetContext(ctxKeyStreamingBody).([]byte); has {
body = append(bufferedStreamingBody, chunk...)
}
r := bytes.NewReader(body)
var events []ConverseStreamEvent
var lastRead int64 = -1
messageBuffer := make([]byte, 1024)
defer func() {
log.Infof("extractAmazonEventStreamEvents: lastRead=%d, r.Size=%d", lastRead, r.Size())
ctx.SetContext(ctxKeyStreamingBody, nil)
}()
for {
msg, err := decodeMessage(r, messageBuffer)
if err != nil {
if err == io.EOF {
break
}
log.Errorf("failed to decode message: %v", err)
break
}
var event ConverseStreamEvent
if err = json.Unmarshal(msg.Payload, &event); err == nil {
events = append(events, event)
}
lastRead = r.Size() - int64(r.Len())
}
return events
}
type bedrockStreamMessage struct {
Headers headers
Payload []byte
}
type EventFrame struct {
TotalLength uint32
HeadersLength uint32
PreludeCRC uint32
Headers map[string]interface{}
Payload []byte
PayloadCRC uint32
}
type headers []header
type header struct {
Name string
Value Value
}
func (hs *headers) Set(name string, value Value) {
var i int
for ; i < len(*hs); i++ {
if (*hs)[i].Name == name {
(*hs)[i].Value = value
return
}
}
*hs = append(*hs, header{
Name: name, Value: value,
})
}
func decodeMessage(reader io.Reader, payloadBuf []byte) (m bedrockStreamMessage, err error) {
crc := crc32.New(crc32.MakeTable(crc32.IEEE))
hashReader := io.TeeReader(reader, crc)
prelude, err := decodePrelude(hashReader, crc)
if err != nil {
return bedrockStreamMessage{}, err
}
if prelude.HeadersLen > 0 {
lr := io.LimitReader(hashReader, int64(prelude.HeadersLen))
m.Headers, err = decodeHeaders(lr)
if err != nil {
return bedrockStreamMessage{}, err
}
}
if payloadLen := prelude.PayloadLen(); payloadLen > 0 {
buf, err := decodePayload(payloadBuf, io.LimitReader(hashReader, int64(payloadLen)))
if err != nil {
return bedrockStreamMessage{}, err
}
m.Payload = buf
}
msgCRC := crc.Sum32()
if err := validateCRC(reader, msgCRC); err != nil {
return bedrockStreamMessage{}, err
}
return m, nil
}
func decodeHeaders(r io.Reader) (headers, error) {
hs := headers{}
for {
name, err := decodeHeaderName(r)
if err != nil {
if err == io.EOF {
// EOF while getting header name means no more headers
break
}
return nil, err
}
value, err := decodeHeaderValue(r)
if err != nil {
return nil, err
}
hs.Set(name, value)
}
return hs, nil
}
func decodeHeaderValue(r io.Reader) (Value, error) {
var raw rawValue
typ, err := decodeUint8(r)
if err != nil {
return nil, err
}
raw.Type = valueType(typ)
var v Value
switch raw.Type {
case stringValueType:
var tv StringValue
err = tv.decode(r)
v = tv
default:
log.Errorf("unknown value type %d", raw.Type)
}
// Error could be EOF, let caller deal with it
return v, err
}
type Value interface {
Get() interface{}
}
type StringValue string
func (v StringValue) Get() interface{} {
return string(v)
}
func (v *StringValue) decode(r io.Reader) error {
s, err := decodeStringValue(r)
if err != nil {
return err
}
*v = StringValue(s)
return nil
}
func decodeBytesValue(r io.Reader) ([]byte, error) {
var raw rawValue
var err error
raw.Len, err = decodeUint16(r)
if err != nil {
return nil, err
}
buf := make([]byte, raw.Len)
_, err = io.ReadFull(r, buf)
if err != nil {
return nil, err
}
return buf, nil
}
func decodeUint16(r io.Reader) (uint16, error) {
var b [2]byte
bs := b[:]
_, err := io.ReadFull(r, bs)
if err != nil {
return 0, err
}
return binary.BigEndian.Uint16(bs), nil
}
func decodeStringValue(r io.Reader) (string, error) {
v, err := decodeBytesValue(r)
return string(v), err
}
type rawValue struct {
Type valueType
Len uint16 // Only set for variable length slices
Value []byte // byte representation of value, BigEndian encoding.
}
type valueType uint8
const (
trueValueType valueType = iota
falseValueType
int8ValueType // Byte
int16ValueType // Short
int32ValueType // Integer
int64ValueType // Long
bytesValueType
stringValueType
timestampValueType
uuidValueType
)
func decodeHeaderName(r io.Reader) (string, error) {
var n headerName
var err error
n.Len, err = decodeUint8(r)
if err != nil {
return "", err
}
name := n.Name[:n.Len]
if _, err := io.ReadFull(r, name); err != nil {
return "", err
}
return string(name), nil
}
func decodeUint8(r io.Reader) (uint8, error) {
type byteReader interface {
ReadByte() (byte, error)
}
if br, ok := r.(byteReader); ok {
v, err := br.ReadByte()
return v, err
}
var b [1]byte
_, err := io.ReadFull(r, b[:])
return b[0], err
}
const maxHeaderNameLen = 255
type headerName struct {
Len uint8
Name [maxHeaderNameLen]byte
}
func decodePayload(buf []byte, r io.Reader) ([]byte, error) {
w := bytes.NewBuffer(buf[0:0])
_, err := io.Copy(w, r)
return w.Bytes(), err
}
type messagePrelude struct {
Length uint32
HeadersLen uint32
PreludeCRC uint32
}
func (p messagePrelude) ValidateLens() error {
if p.Length == 0 {
return fmt.Errorf("message prelude want: 16, have: %v", int(p.Length))
}
return nil
}
func (p messagePrelude) PayloadLen() uint32 {
return p.Length - p.HeadersLen - 16
}
func decodePrelude(r io.Reader, crc hash.Hash32) (messagePrelude, error) {
var p messagePrelude
var err error
p.Length, err = decodeUint32(r)
if err != nil {
return messagePrelude{}, err
}
p.HeadersLen, err = decodeUint32(r)
if err != nil {
return messagePrelude{}, err
}
if err := p.ValidateLens(); err != nil {
return messagePrelude{}, err
}
preludeCRC := crc.Sum32()
if err := validateCRC(r, preludeCRC); err != nil {
return messagePrelude{}, err
}
p.PreludeCRC = preludeCRC
return p, nil
}
func decodeUint32(r io.Reader) (uint32, error) {
var b [4]byte
bs := b[:]
_, err := io.ReadFull(r, bs)
if err != nil {
return 0, err
}
return binary.BigEndian.Uint32(bs), nil
}
func validateCRC(r io.Reader, expect uint32) error {
msgCRC, err := decodeUint32(r)
if err != nil {
return err
}
if msgCRC != expect {
return fmt.Errorf("message checksum mismatch")
}
return nil
}
func (b *bedrockProvider) TransformResponseHeaders(ctx wrapper.HttpContext, apiName ApiName, headers http.Header) {
ctx.SetContext("X-Amzn-Requestid", headers.Get("X-Amzn-Requestid"))
if headers.Get("Content-Type") == "application/vnd.amazon.eventstream" {
headers.Set("Content-Type", "text/event-stream; charset=utf-8")
}
headers.Del("Content-Length")
}
func (b *bedrockProvider) GetProviderType() string {
return providerTypeBedrock
}
func (b *bedrockProvider) OnRequestHeaders(ctx wrapper.HttpContext, apiName ApiName) error {
b.config.handleRequestHeaders(b, ctx, apiName)
return nil
}
func (b *bedrockProvider) TransformRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, headers http.Header) {
util.OverwriteRequestHostHeader(headers, fmt.Sprintf(bedrockDefaultDomain, b.config.awsRegion))
}
func (b *bedrockProvider) OnRequestBody(ctx wrapper.HttpContext, apiName ApiName, body []byte) (types.Action, error) {
if !b.config.isSupportedAPI(apiName) {
return types.ActionContinue, errUnsupportedApiName
}
return b.config.handleRequestBody(b, b.contextCache, ctx, apiName, body)
}
func (b *bedrockProvider) insertHttpContextMessage(body []byte, content string, onlyOneSystemBeforeFile bool) ([]byte, error) {
request := &bedrockTextGenRequest{}
if err := json.Unmarshal(body, request); err != nil {
return nil, fmt.Errorf("unable to unmarshal request: %v", err)
}
if len(request.System) > 0 {
request.System = append(request.System, systemContentBlock{Text: content})
} else {
request.System = []systemContentBlock{{Text: content}}
}
requestBytes, err := json.Marshal(request)
b.setAuthHeaders(requestBytes, nil)
return requestBytes, err
}
func (b *bedrockProvider) TransformRequestBodyHeaders(ctx wrapper.HttpContext, apiName ApiName, body []byte, headers http.Header) ([]byte, error) {
switch apiName {
case ApiNameChatCompletion:
return b.onChatCompletionRequestBody(ctx, body, headers)
default:
return b.config.defaultTransformRequestBody(ctx, apiName, body)
}
}
func (b *bedrockProvider) TransformResponseBody(ctx wrapper.HttpContext, apiName ApiName, body []byte) ([]byte, error) {
if apiName == ApiNameChatCompletion {
return b.onChatCompletionResponseBody(ctx, body)
}
return nil, errUnsupportedApiName
}
func (b *bedrockProvider) onChatCompletionResponseBody(ctx wrapper.HttpContext, body []byte) ([]byte, error) {
bedrockResponse := &bedrockConverseResponse{}
if err := json.Unmarshal(body, bedrockResponse); err != nil {
log.Errorf("unable to unmarshal bedrock response: %v", err)
return nil, fmt.Errorf("unable to unmarshal bedrock response: %v", err)
}
response := b.buildChatCompletionResponse(ctx, bedrockResponse)
return json.Marshal(response)
}
func (b *bedrockProvider) onChatCompletionRequestBody(ctx wrapper.HttpContext, body []byte, headers http.Header) ([]byte, error) {
request := &chatCompletionRequest{}
err := b.config.parseRequestAndMapModel(ctx, request, body)
if err != nil {
return nil, err
}
streaming := request.Stream
headers.Set("Accept", "*/*")
if streaming {
util.OverwriteRequestPathHeader(headers, fmt.Sprintf(bedrockStreamChatCompletionPath, request.Model))
} else {
util.OverwriteRequestPathHeader(headers, fmt.Sprintf(bedrockChatCompletionPath, request.Model))
}
return b.buildBedrockTextGenerationRequest(request, headers)
}
func (b *bedrockProvider) buildBedrockTextGenerationRequest(origRequest *chatCompletionRequest, headers http.Header) ([]byte, error) {
messages := make([]bedrockMessage, 0, len(origRequest.Messages))
for i := range origRequest.Messages {
messages = append(messages, chatMessage2BedrockMessage(origRequest.Messages[i]))
}
request := &bedrockTextGenRequest{
Messages: messages,
InferenceConfig: bedrockInferenceConfig{
MaxTokens: origRequest.MaxTokens,
Temperature: origRequest.Temperature,
TopP: origRequest.TopP,
},
AdditionalModelRequestFields: map[string]interface{}{},
PerformanceConfig: PerformanceConfiguration{
Latency: "standard",
},
}
requestBytes, err := json.Marshal(request)
b.setAuthHeaders(requestBytes, headers)
return requestBytes, err
}
func (b *bedrockProvider) buildChatCompletionResponse(ctx wrapper.HttpContext, bedrockResponse *bedrockConverseResponse) *chatCompletionResponse {
var outputContent string
if len(bedrockResponse.Output.Message.Content) > 0 {
outputContent = bedrockResponse.Output.Message.Content[0].Text
}
choices := []chatCompletionChoice{
{
Index: 0,
Message: &chatMessage{
Role: bedrockResponse.Output.Message.Role,
Content: outputContent,
},
FinishReason: stopReasonBedrock2OpenAI(bedrockResponse.StopReason),
},
}
requestId := ctx.GetStringContext("X-Amzn-Requestid", "")
return &chatCompletionResponse{
Id: requestId,
Created: time.Now().UnixMilli() / 1000,
Model: ctx.GetStringContext(ctxKeyFinalRequestModel, ""),
SystemFingerprint: "",
Object: objectChatCompletion,
Choices: choices,
Usage: usage{
PromptTokens: bedrockResponse.Usage.InputTokens,
CompletionTokens: bedrockResponse.Usage.OutputTokens,
TotalTokens: bedrockResponse.Usage.TotalTokens,
},
}
}
func stopReasonBedrock2OpenAI(reason string) string {
switch reason {
case "end_turn":
return finishReasonStop
case "stop_sequence":
return finishReasonStop
case "max_tokens":
return finishReasonLength
default:
return reason
}
}
type bedrockTextGenRequest struct {
Messages []bedrockMessage `json:"messages"`
System []systemContentBlock `json:"system,omitempty"`
InferenceConfig bedrockInferenceConfig `json:"inferenceConfig,omitempty"`
AdditionalModelRequestFields map[string]interface{} `json:"additionalModelRequestFields,omitempty"`
PerformanceConfig PerformanceConfiguration `json:"performanceConfig,omitempty"`
}
type PerformanceConfiguration struct {
Latency string `json:"latency,omitempty"`
}
type bedrockMessage struct {
Role string `json:"role"`
Content []bedrockMessageContent `json:"content"`
}
type bedrockMessageContent struct {
Text string `json:"text,omitempty"`
Image *imageBlock `json:"image,omitempty"`
}
type systemContentBlock struct {
Text string `json:"text,omitempty"`
}
type imageBlock struct {
Format string `json:"format,omitempty"`
Source imageSource `json:"source,omitempty"`
}
type imageSource struct {
Bytes string `json:"bytes,omitempty"`
}
type bedrockInferenceConfig struct {
StopSequences []string `json:"stopSequences,omitempty"`
MaxTokens int `json:"maxTokens,omitempty"`
Temperature float64 `json:"temperature,omitempty"`
TopP float64 `json:"topP,omitempty"`
}
type bedrockConverseResponse struct {
Metrics converseMetrics `json:"metrics"`
Output converseOutputMemberMessage `json:"output"`
StopReason string `json:"stopReason"`
Usage tokenUsage `json:"usage"`
}
type converseMetrics struct {
LatencyMs int `json:"latencyMs"`
}
type converseOutputMemberMessage struct {
Message message `json:"message"`
}
type message struct {
Content []contentBlockMemberText `json:"content"`
Role string `json:"role"`
}
type contentBlockMemberText struct {
Text string `json:"text"`
}
type tokenUsage struct {
InputTokens int `json:"inputTokens,omitempty"`
OutputTokens int `json:"outputTokens,omitempty"`
TotalTokens int `json:"totalTokens"`
}
func chatMessage2BedrockMessage(chatMessage chatMessage) bedrockMessage {
if chatMessage.IsStringContent() {
return bedrockMessage{
Role: chatMessage.Role,
Content: []bedrockMessageContent{{Text: chatMessage.StringContent()}},
}
} else {
var contents []bedrockMessageContent
openaiContent := chatMessage.ParseContent()
for _, part := range openaiContent {
var content bedrockMessageContent
if part.Type == contentTypeText {
content.Text = part.Text
} else {
log.Warnf("imageUrl is not supported: %s", part.Type)
continue
}
contents = append(contents, content)
}
return bedrockMessage{
Role: chatMessage.Role,
Content: contents,
}
}
}
func (b *bedrockProvider) setAuthHeaders(body []byte, headers http.Header) {
t := time.Now().UTC()
amzDate := t.Format("20060102T150405Z")
dateStamp := t.Format("20060102")
path, _ := proxywasm.GetHttpRequestHeader(":path")
if headers != nil {
path = headers.Get(":path")
}
signature := b.generateSignature(path, amzDate, dateStamp, body)
if headers != nil {
headers.Set("X-Amz-Date", amzDate)
headers.Set("Authorization", fmt.Sprintf("AWS4-HMAC-SHA256 Credential=%s/%s/%s/%s/aws4_request, SignedHeaders=%s, Signature=%s", b.config.awsAccessKey, dateStamp, b.config.awsRegion, awsService, bedrockSignedHeaders, signature))
} else {
_ = proxywasm.ReplaceHttpRequestHeader("X-Amz-Date", amzDate)
_ = proxywasm.ReplaceHttpRequestHeader("Authorization", fmt.Sprintf("AWS4-HMAC-SHA256 Credential=%s/%s/%s/%s/aws4_request, SignedHeaders=%s, Signature=%s", b.config.awsAccessKey, dateStamp, b.config.awsRegion, awsService, bedrockSignedHeaders, signature))
}
}
func (b *bedrockProvider) generateSignature(path, amzDate, dateStamp string, body []byte) string {
hashedPayload := sha256Hex(body)
path = urlEncoding(path)
endpoint := fmt.Sprintf(bedrockDefaultDomain, b.config.awsRegion)
canonicalHeaders := fmt.Sprintf("host:%s\nx-amz-date:%s\n", endpoint, amzDate)
canonicalRequest := fmt.Sprintf("%s\n%s\n\n%s\n%s\n%s",
httpPostMethod, path, canonicalHeaders, bedrockSignedHeaders, hashedPayload)
credentialScope := fmt.Sprintf("%s/%s/%s/aws4_request", dateStamp, b.config.awsRegion, awsService)
hashedCanonReq := sha256Hex([]byte(canonicalRequest))
stringToSign := fmt.Sprintf("AWS4-HMAC-SHA256\n%s\n%s\n%s",
amzDate, credentialScope, hashedCanonReq)
signingKey := getSignatureKey(b.config.awsSecretKey, dateStamp, b.config.awsRegion, awsService)
signature := hmacHex(signingKey, stringToSign)
return signature
}
func urlEncoding(rawStr string) string {
encodedStr := strings.ReplaceAll(rawStr, ":", "%3A")
encodedStr = strings.ReplaceAll(encodedStr, "+", "%2B")
encodedStr = strings.ReplaceAll(encodedStr, "=", "%3D")
encodedStr = strings.ReplaceAll(encodedStr, "&", "%26")
encodedStr = strings.ReplaceAll(encodedStr, "$", "%24")
encodedStr = strings.ReplaceAll(encodedStr, "@", "%40")
return encodedStr
}
func getSignatureKey(key, dateStamp, region, service string) []byte {
kDate := hmacSha256([]byte("AWS4"+key), dateStamp)
kRegion := hmacSha256(kDate, region)
kService := hmacSha256(kRegion, service)
kSigning := hmacSha256(kService, "aws4_request")
return kSigning
}
func hmacSha256(key []byte, data string) []byte {
h := hmac.New(sha256.New, key)
h.Write([]byte(data))
return h.Sum(nil)
}
func sha256Hex(data []byte) string {
h := sha256.New()
h.Write(data)
return hex.EncodeToString(h.Sum(nil))
}
func hmacHex(key []byte, data string) string {
h := hmac.New(sha256.New, key)
h.Write([]byte(data))
return hex.EncodeToString(h.Sum(nil))
}

View File

@@ -68,6 +68,7 @@ const (
providerTypeCoze = "coze"
providerTypeTogetherAI = "together-ai"
providerTypeDify = "dify"
providerTypeBedrock = "bedrock"
protocolOpenAI = "openai"
protocolOriginal = "original"
@@ -138,6 +139,7 @@ var (
providerTypeCoze: &cozeProviderInitializer{},
providerTypeTogetherAI: &togetherAIProviderInitializer{},
providerTypeDify: &difyProviderInitializer{},
providerTypeBedrock: &bedrockProviderInitializer{},
}
)
@@ -242,6 +244,15 @@ type ProviderConfig struct {
// @Title zh-CN hunyuan api id for authorization
// @Description zh-CN 仅适用于Hun Yuan AI服务鉴权
hunyuanAuthId string `required:"false" yaml:"hunyuanAuthId" json:"hunyuanAuthId"`
// @Title zh-CN Amazon Bedrock AccessKey for authorization
// @Description zh-CN 仅适用于Amazon Bedrock服务鉴权API key/id 参考https://docs.aws.amazon.com/zh_cn/IAM/latest/UserGuide/reference_sigv.html
awsAccessKey string `required:"false" yaml:"awsAccessKey" json:"awsAccessKey"`
// @Title zh-CN Amazon Bedrock SecretKey for authorization
// @Description zh-CN 仅适用于Amazon Bedrock服务鉴权
awsSecretKey string `required:"false" yaml:"awsSecretKey" json:"awsSecretKey"`
// @Title zh-CN Amazon Bedrock Region
// @Description zh-CN 仅适用于Amazon Bedrock服务访问
awsRegion string `required:"false" yaml:"awsRegion" json:"awsRegion"`
// @Title zh-CN minimax API type
// @Description zh-CN 仅适用于 minimax 服务。minimax API 类型v2 和 pro 中选填一项,默认值为 v2
minimaxApiType string `required:"false" yaml:"minimaxApiType" json:"minimaxApiType"`
@@ -346,6 +357,9 @@ func (c *ProviderConfig) FromJson(json gjson.Result) {
c.claudeVersion = json.Get("claudeVersion").String()
c.hunyuanAuthId = json.Get("hunyuanAuthId").String()
c.hunyuanAuthKey = json.Get("hunyuanAuthKey").String()
c.awsAccessKey = json.Get("awsAccessKey").String()
c.awsSecretKey = json.Get("awsSecretKey").String()
c.awsRegion = json.Get("awsRegion").String()
c.minimaxApiType = json.Get("minimaxApiType").String()
c.minimaxGroupId = json.Get("minimaxGroupId").String()
c.cloudflareAccountId = json.Get("cloudflareAccountId").String()

View File

@@ -75,18 +75,22 @@ description: higress 支持通过集成搜索引擎Google/Bing/Arxiv/Elastics
## Elasticsearch 特定配置
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|------|----------|----------|--------|-----------------------|
| index | string | 必填 | - | 要搜索的Elasticsearch索引名称 |
| contentField | string | 必填 | - | 要查询的内容字段名称 |
| semanticTextField | string | 必填 | - | 要查询的 embedding 字段名称 |
| linkField | string | 必填 | - | 结果链接字段名称 |
| titleField | string | 必填 | - | 结果标题字段名称 |
| username | string | 选填 | - | Elasticsearch 用户名 |
| password | string | 选填 | - | Elasticsearch 密码 |
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|------|----------|------|--------|------------------------------------|
| index | string | 必填 | - | 要搜索的 Elasticsearch 索引名称 |
| contentField | string | 必填 | - | 要查询的内容字段名称 |
| semanticTextField | string | 必填 | - | 要查询的 embedding 字段名称 |
| linkField | string | 选填 | - | 结果链接字段名称,当配置 `needReference` 时需要填写 |
| titleField | string | 选填 | - | 结果标题字段名称,当配置 `needReference` 时需要填写 |
| username | string | 选填 | - | Elasticsearch 用户名 |
| password | string | 选填 | - | Elasticsearch 密码 |
混合搜索中使用的 [Reciprocal Rank Fusion (RRF)](https://www.elastic.co/guide/en/elasticsearch/reference/8.17/rrf.html) 查询要求 Elasticsearch 的版本在 8.8 及以上。
目前文档向量化依赖于 Elasticsearch 的 Embedding 模型,该功能需要 Elasticsearch 企业版 License或可使用 30 天的 Trial License。安装 Elasticsearch 内置 Embedding 模型的步骤可参考[该文档](https://www.elastic.co/docs/explore-analyze/machine-learning/nlp/ml-nlp-elser#alternative-download-deploy);若需安装第三方 Embedding 模型,可参考[该文档](https://www.elastic.co/docs/explore-analyze/machine-learning/nlp/ml-nlp-text-emb-vector-search-example)。
有关 ai-search 插件集成 Elasticsearch 的完整教程,请参考:[使用 LangChain + Higress + Elasticsearch 构建 RAG 应用](https://cr7258.github.io/blogs/original/2025/15-rag-higress-es-langchain)。
## Quark 特定配置
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
@@ -204,13 +208,9 @@ searchFrom:
searchFrom:
- type: elasticsearch
serviceName: "es-svc.static"
# 固定地址服务的端口默认是80
servicePort: 80
index: "knowledge_base"
contentField: "content"
semanticTextField: "semantic_text"
linkField: "url"
titleField: "title"
# username: "elastic"
# password: "password"
```

View File

@@ -80,13 +80,17 @@ It is strongly recommended to enable this feature when using Arxiv or Elasticsea
| index | string | Required | - | Elasticsearch index name to search |
| contentField | string | Required | - | Content field name to query |
| semanticTextField | string | Required | - | Embedding field name to query |
| linkField | string | Required | - | Result link field name |
| titleField | string | Required | - | Result title field name |
| linkField | string | Optional | - | Result link field name, needed when `needReference` is configured |
| titleField | string | Optional | - | Result title field name, needed when `needReference` is configured |
| username | string | Optional | - | Elasticsearch username |
| password | string | Optional | - | Elasticsearch password |
The [Reciprocal Rank Fusion (RRF)](https://www.elastic.co/guide/en/elasticsearch/reference/8.17/rrf.html) query used in hybrid search requires Elasticsearch version 8.8 or higher.
Currently, document vectorization relies on Elasticsearch's embedding model, which requires an Elasticsearch Enterprise license or a 30-day Trial license. To install the built-in embedding model in Elasticsearch, please refer to [this documentation](https://www.elastic.co/docs/explore-analyze/machine-learning/nlp/ml-nlp-elser#alternative-download-deploy). If you want to install a third-party embedding model, please refer to [this guide](https://www.elastic.co/docs/explore-analyze/machine-learning/nlp/ml-nlp-text-emb-vector-search-example).
For a complete tutorial on integrating the ai-search plugin with Elasticsearch, please refer to: [Building a RAG Application with LangChain + Higress + Elasticsearch](https://cr7258.github.io/blogs/original/2025/15-rag-higress-es-langchain).
## Quark Specific Configuration
| Name | Data Type | Requirement | Default Value | Description |
@@ -203,13 +207,9 @@ Note that excessive concurrency may lead to rate limiting, adjust according to a
searchFrom:
- type: elasticsearch
serviceName: "es-svc.static"
# static ip service use 80 as default port
servicePort: 80
index: "knowledge_base"
contentField: "content"
semanticTextField: "semantic_text"
linkField: "url"
titleField: "title"
# username: "elastic"
# password: "password"
```

View File

@@ -27,7 +27,7 @@ type ElasticsearchSearch struct {
password string
}
func NewElasticsearchSearch(config *gjson.Result) (*ElasticsearchSearch, error) {
func NewElasticsearchSearch(config *gjson.Result, needReference bool) (*ElasticsearchSearch, error) {
engine := &ElasticsearchSearch{}
serviceName := config.Get("serviceName").String()
if serviceName == "" {
@@ -35,7 +35,13 @@ func NewElasticsearchSearch(config *gjson.Result) (*ElasticsearchSearch, error)
}
servicePort := config.Get("servicePort").Int()
if servicePort == 0 {
return nil, errors.New("servicePort not found")
if strings.HasSuffix(serviceName, ".static") {
servicePort = 80
} else if strings.HasSuffix(serviceName, ".dns") {
servicePort = 443
} else {
return nil, errors.New("servicePort not found")
}
}
engine.client = wrapper.NewClusterClient(wrapper.FQDNCluster{
FQDN: serviceName,
@@ -54,14 +60,18 @@ func NewElasticsearchSearch(config *gjson.Result) (*ElasticsearchSearch, error)
if engine.semanticTextField == "" {
return nil, errors.New("semanticTextField not found")
}
engine.linkField = config.Get("linkField").String()
if engine.linkField == "" {
return nil, errors.New("linkField not found")
}
engine.titleField = config.Get("titleField").String()
if engine.titleField == "" {
return nil, errors.New("titleField not found")
if needReference {
engine.linkField = config.Get("linkField").String()
if engine.linkField == "" {
return nil, errors.New("linkField not found")
}
engine.titleField = config.Get("titleField").String()
if engine.titleField == "" {
return nil, errors.New("titleField not found")
}
}
engine.timeoutMillisecond = uint32(config.Get("timeoutMillisecond").Uint())
if engine.timeoutMillisecond == 0 {
engine.timeoutMillisecond = 5000
@@ -93,6 +103,9 @@ func (e ElasticsearchSearch) generateAuthorizationHeader() string {
func (e ElasticsearchSearch) generateQueryBody(ctx engine.SearchContext) string {
queryText := strings.Join(ctx.Querys, " ")
return fmt.Sprintf(`{
"_source":{
"excludes": "%s"
},
"retriever": {
"rrf": {
"retrievers": [
@@ -118,7 +131,7 @@ func (e ElasticsearchSearch) generateQueryBody(ctx engine.SearchContext) string
]
}
}
}`, e.contentField, queryText, e.semanticTextField, queryText)
}`, e.semanticTextField, e.contentField, queryText, e.semanticTextField, queryText)
}
func (e ElasticsearchSearch) CallArgs(ctx engine.SearchContext) engine.CallArgs {
@@ -145,9 +158,7 @@ func (e ElasticsearchSearch) ParseResult(ctx engine.SearchContext, response []by
Link: source.Get(e.linkField).String(),
Content: source.Get(e.contentField).String(),
}
if result.Valid() {
results = append(results, result)
}
results = append(results, result)
}
return results
}

View File

@@ -185,7 +185,7 @@ func parseConfig(json gjson.Result, config *Config, log wrapper.Log) error {
arxivExists = true
onlyQuark = false
case "elasticsearch":
searchEngine, err := elasticsearch.NewElasticsearchSearch(&e)
searchEngine, err := elasticsearch.NewElasticsearchSearch(&e, config.needReference)
if err != nil {
return fmt.Errorf("elasticsearch search engine init failed:%s", err)
}

View File

@@ -17,4 +17,5 @@ require (
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tidwall/resp v0.1.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
)

View File

@@ -9,6 +9,7 @@ github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94=
github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
@@ -17,4 +18,6 @@ github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@@ -46,7 +46,7 @@ const (
ResponseStreamingBody = "response_streaming_body"
ResponseBody = "response_body"
// Inner metric & log attributes name
// Inner metric & log attributes
Model = "model"
InputToken = "input_token"
OutputToken = "output_token"
@@ -55,6 +55,16 @@ const (
LLMDurationCount = "llm_duration_count"
LLMStreamDurationCount = "llm_stream_duration_count"
ResponseType = "response_type"
ChatID = "chat_id"
ChatRound = "chat_round"
// Inner span attributes
ArmsSpanKind = "gen_ai.span.kind"
ArmsModelName = "gen_ai.model_name"
ArmsRequestModel = "gen_ai.request.model"
ArmsInputToken = "gen_ai.usage.input_tokens"
ArmsOutputToken = "gen_ai.usage.output_tokens"
ArmsTotalToken = "gen_ai.usage.total_tokens"
// Extract Rule
RuleFirst = "first"
@@ -171,7 +181,8 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, config AIStatisticsConfig, lo
setAttributeBySource(ctx, config, FixedValue, nil, log)
// Set user defined log & span attributes which type is request_header
setAttributeBySource(ctx, config, RequestHeader, nil, log)
// Set request start time.
// Set span attributes for ARMS.
setSpanAttribute(ArmsSpanKind, "LLM", log)
return types.ActionContinue
}
@@ -179,6 +190,22 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, config AIStatisticsConfig, lo
func onHttpRequestBody(ctx wrapper.HttpContext, config AIStatisticsConfig, body []byte, log wrapper.Log) types.Action {
// Set user defined log & span attributes.
setAttributeBySource(ctx, config, RequestBody, body, log)
// Set span attributes for ARMS.
requestModel := gjson.GetBytes(body, "model").String()
if requestModel == "" {
requestModel = "UNKNOWN"
}
setSpanAttribute(ArmsRequestModel, requestModel, log)
// Set the number of conversation rounds
if gjson.GetBytes(body, "messages").Exists() {
userPromptCount := 0
for _, msg := range gjson.GetBytes(body, "messages").Array() {
if msg.Get("role").String() == "user" {
userPromptCount += 1
}
}
ctx.SetUserAttribute(ChatRound, userPromptCount)
}
// Write log
ctx.WriteUserAttributeToLogWithKey(wrapper.AILogKey)
@@ -211,6 +238,10 @@ func onHttpStreamingBody(ctx wrapper.HttpContext, config AIStatisticsConfig, dat
}
ctx.SetUserAttribute(ResponseType, "stream")
chatID := gjson.GetBytes(data, "id").String()
if chatID != "" {
ctx.SetUserAttribute(ChatID, chatID)
}
// Get requestStartTime from http context
requestStartTime, ok := ctx.GetContext(StatisticsRequestStartTime).(int64)
@@ -231,6 +262,11 @@ func onHttpStreamingBody(ctx wrapper.HttpContext, config AIStatisticsConfig, dat
ctx.SetUserAttribute(Model, model)
ctx.SetUserAttribute(InputToken, inputToken)
ctx.SetUserAttribute(OutputToken, outputToken)
// Set span attributes for ARMS.
setSpanAttribute(ArmsModelName, model, log)
setSpanAttribute(ArmsInputToken, inputToken, log)
setSpanAttribute(ArmsOutputToken, outputToken, log)
setSpanAttribute(ArmsTotalToken, inputToken+outputToken, log)
}
// If the end of the stream is reached, record metrics/logs/spans.
if endOfStream {
@@ -263,12 +299,21 @@ func onHttpResponseBody(ctx wrapper.HttpContext, config AIStatisticsConfig, body
ctx.SetUserAttribute(LLMServiceDuration, responseEndTime-requestStartTime)
ctx.SetUserAttribute(ResponseType, "normal")
chatID := gjson.GetBytes(body, "id").String()
if chatID != "" {
ctx.SetUserAttribute(ChatID, chatID)
}
// Set information about this request
if model, inputToken, outputToken, ok := getUsage(body); ok {
ctx.SetUserAttribute(Model, model)
ctx.SetUserAttribute(InputToken, inputToken)
ctx.SetUserAttribute(OutputToken, outputToken)
// Set span attributes for ARMS.
setSpanAttribute(ArmsModelName, model, log)
setSpanAttribute(ArmsInputToken, inputToken, log)
setSpanAttribute(ArmsOutputToken, outputToken, log)
setSpanAttribute(ArmsTotalToken, inputToken+outputToken, log)
}
// Set user defined log & span attributes.
@@ -283,8 +328,14 @@ func onHttpResponseBody(ctx wrapper.HttpContext, config AIStatisticsConfig, body
return types.ActionContinue
}
func unifySSEChunk(data []byte) []byte {
data = bytes.ReplaceAll(data, []byte("\r\n"), []byte("\n"))
data = bytes.ReplaceAll(data, []byte("\r"), []byte("\n"))
return data
}
func getUsage(data []byte) (model string, inputTokenUsage int64, outputTokenUsage int64, ok bool) {
chunks := bytes.Split(bytes.TrimSpace(data), []byte("\n\n"))
chunks := bytes.Split(bytes.TrimSpace(unifySSEChunk(data)), []byte("\n\n"))
for _, chunk := range chunks {
// the feature strings are used to identify the usage data, like:
// {"model":"gpt2","usage":{"prompt_tokens":1,"completion_tokens":1}}
@@ -353,7 +404,7 @@ func setAttributeBySource(ctx wrapper.HttpContext, config AIStatisticsConfig, so
}
func extractStreamingBodyByJsonPath(data []byte, jsonPath string, rule string, log wrapper.Log) interface{} {
chunks := bytes.Split(bytes.TrimSpace(data), []byte("\n\n"))
chunks := bytes.Split(bytes.TrimSpace(unifySSEChunk(data)), []byte("\n\n"))
var value interface{}
if rule == RuleFirst {
for _, chunk := range chunks {

View File

@@ -26,8 +26,9 @@ func claimsToHeader(claims map[string]any, cth []cfg.ClaimsToHeader) {
if v, ok := claims[cth[i].Claim]; ok {
if *cth[i].Override {
proxywasm.ReplaceHttpRequestHeader(cth[i].Header, fmt.Sprint(v))
} else {
proxywasm.AddHttpRequestHeader(cth[i].Header, fmt.Sprint(v))
}
proxywasm.AddHttpRequestHeader(cth[i].Header, fmt.Sprint(v))
}
}
}

View File

@@ -1,5 +1,9 @@
# MCP Server Implementation Guide
> **🌟 Try it now!** Experience Higress-hosted Remote MCP Servers at [https://mcp.higress.ai/](https://mcp.higress.ai/). This platform allows you to see firsthand how Higress can host and manage Remote MCP Servers.
>
> ![Higress MCP Server Platform](https://img.alicdn.com/imgextra/i2/O1CN01nmVa0a1aChgpyyWOX_!!6000000003294-0-tps-3430-1742.jpg)
## Background
Higress, as an Envoy-based API gateway, supports hosting MCP Servers through its plugin mechanism. MCP (Model Context Protocol) is essentially an AI-friendly API that enables AI Agents to more easily call various tools and services. Higress provides unified capabilities for authentication, authorization, rate limiting, and observability for tool calls, simplifying the development and deployment of AI applications.

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.20250413143111-22e90c61ae13
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250423015849-23258157a406
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.20250413143111-22e90c61ae13 h1:o1XeVR4EPPcdUrogcCcgJbh1cgK80x1ecYRPFypWkfw=
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250413143111-22e90c61ae13/go.mod h1:VsSgC7oa1dhUArj6COnzo2AWSMCuNQAFo5HjZdo5Rks=
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/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

@@ -0,0 +1,12 @@
# Agricultural Product Price Inquiry
The APP Code required for API authentication can be applied for on the Alibaba Cloud API Marketplace: https://market.aliyun.com/apimarket/detail/cmapi00044839
# Agricultural Product Price Inquiry Server Configuration Document
This document aims to provide users with an overview of the basic functions of the Agricultural Product Price Inquiry MCP (Market Cloud Platform) server and a detailed introduction to the tools used. Through this document, users can learn how to use these tools to obtain agricultural product price information for specific regions and categories.
## Function Overview
The `agricultural-product-price-query` service is specifically designed to fetch the latest market price information for agricultural products from the Alibaba Cloud API Marketplace. It allows users to query relevant price statistical data based on specified geographic regions, product categories, and dates. This service is very useful for agricultural practitioners, research institutions, or any stakeholders who need to understand the latest market price trends.

View File

@@ -0,0 +1,23 @@
# 农产品价格查询
API认证需要的APP Code请在阿里云API市场申请: https://market.aliyun.com/apimarket/detail/cmapi00064763
## 什么是云市场API MCP服务
阿里云云市场是生态伙伴的交易服务平台我们致力于为合作伙伴提供覆盖上云、商业化和售卖的全链路服务帮助客户高效获取、部署和管理优质生态产品。云市场的API服务涵盖以下几个类目应用开发、身份验证与金融、车辆交通与物流、企业服务、短信与运营商、AI应用与OCR、生活服务。
云市场API依托Higress提供MCP服务您只需在云市场完成订阅并获取AppCode通过Higress MCP Server进行配置即可无缝集成云市场API服务。
## 如何在使用云市场API MCP服务
1. 进入API详情页订阅该API。您可以优先使用免费试用。
2. 前往云市场用户控制台使用阿里云账号登陆后查看已订阅API服务的AppCode并配置到Higress MCP Server的配置中。注意在阿里云市场订阅API服务后您将获得AppCode。对于您订阅的所有API服务此AppCode是相同的您只需使用这一个AppCode即可访问所有已订阅的API服务。
3. 云市场用户控制台会实时展示已订阅的预付费API服务的可用额度如您免费试用额度已用完您可以选择重新订阅。
# 农产品价格查询服务器配置文档
本文件旨在为用户提供关于农产品价格查询MCPMarket Cloud Platform服务器的基本功能概述以及所使用工具的具体介绍。通过这份文档用户可以了解到如何利用这些工具来获取特定地区及类目的农产品价格信息。
## 功能简介
`agricultural-product-price-query` 服务专门设计用于从阿里云API市场获取最新的农产品市场价格信息。它允许用户根据指定的地理区域、产品类别和日期查询相关的价格统计数据。此服务对于农业从业者、研究机构或任何需要了解最新市场价格趋势的利益相关者都非常有用。

View File

@@ -0,0 +1,130 @@
{
"info": {
"description": "惠农行情数据接口可提供全国31个省级行政区、2818个县级行政区的蔬菜、水果、畜禽、水产等3000多个品类2万多个常见农产品的价格数据服务、历史数据最早可追溯至2013年。惠农行情大数据是基于惠农网的线上电商交易平台线下行情官系统和农业专家团队所发布的农产品信息经标准化清洗、分类和数据库建设而形成的农业全产业链专业数据库是农产品生产经营管理监测、分析、预警的有力工具。",
"title": "农产品价格行情数据接口",
"version": "1.0.0"
},
"openapi": "3.0.1",
"paths": {
"/aliyun/market/category/detail": {
"get": {
"operationId": "类目数据价格行情",
"summary": "阿里云api市场-1日数据统计信息(类目)",
"parameters": [
{
"description": "中国地理行政区县级市支持地区下载路径https://files.cnhnb.com/area_list.xlsx",
"example": "岳麓区",
"in": "query",
"name": "areaName",
"required": true,
"schema": {
"type": "string"
}
},
{
"description": "农产品三级类目名称具体名单参照https://files.cnhnb.com/category_list.xlsx",
"example": "大米",
"in": "query",
"name": "categoryName",
"required": true,
"schema": {
"type": "string"
}
},
{
"description": "要查询的日期。 例子: 2020-12-25",
"example": "2020-12-22",
"in": "query",
"name": "date",
"required": false,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"description": "响应状态码",
"example": 0
},
"msg": {
"type": "string",
"description": "响应消息",
"example": "success"
},
"traceId": {
"type": "string",
"description": "跟踪ID",
"example": "77e70b08b4ca064d"
},
"data": {
"type": "array",
"items": {
"type": "object",
"properties": {
"cateName": {
"type": "string",
"description": "类别名称",
"example": "大米"
},
"breedName": {
"type": "string",
"description": "品种名称",
"example": "有机米"
},
"unit": {
"type": "string",
"description": "单位",
"example": "斤"
},
"nowAvgPrice": {
"type": "number",
"description": "当前平均价格",
"example": 2.8
},
"provinceName": {
"type": "string",
"description": "省份名称",
"example": "湖南省"
},
"cityName": {
"type": "string",
"description": "城市名称",
"example": "长沙市"
},
"areaName": {
"type": "string",
"description": "区域名称",
"example": "岳麓区"
},
"marketDate": {
"type": "string",
"description": "市场日期",
"example": "2020-12-24T00:00:00Z"
}
}
}
}
}
}
}
},
"description": "成功响应"
}
}
}
}
},
"servers": [
{
"url": "https://agroprice.market.alicloudapi.com"
}
]
}

View File

@@ -0,0 +1,151 @@
server:
name: agricultural-product-price-query
config:
appCode: ""
tools:
- name: avg-price
description: 地区均价
args:
- name: city
description: 地级市名称
type: string
position: body
- name: code
description: 农产品代码通过【支持产品查询】接口获取的code
type: string
required: true
position: body
- name: province
description: 省份名称,暂不支持港澳台地区,省份名字不带“省”字,譬如:浙江省,输入浙江
type: string
required: true
position: body
requestTemplate:
url: https://lhncpcx.market.alicloudapi.com/agricultural/products/region/average-price
method: POST
headers:
- key: Content-Type
value: application/x-www-form-urlencoded
- key: Authorization
value: APPCODE {{.config.appCode}}
- key: X-Ca-Nonce
value: '{{uuidv4}}'
responseTemplate:
prependBody: |+
# API Response Information
Below is the response from an API call. To help you understand the data, I've provided:
1. A detailed description of all fields in the response structure
2. The complete API response
## Response Structure
> Content-Type: application/json
- **code**: (Type: integer)
- **data**: (Type: object)
- **data.avg**: 参考均价 (Type: string)
- **data.sample**: 样本数量 (Type: string)
- **data.unit**: 计价单位 (Type: string)
- **msg**: (Type: string)
- **taskNo**: (Type: string)
## Original Response
- name: product-query
description: 支持产品查询
args:
- name: name
description: 农产品名称,支持模糊查询
type: string
position: body
- name: type
description: 农产品种类1表示畜产2表示水产3代表粮油4代表果品5代表蔬菜
type: string
required: true
position: body
requestTemplate:
url: https://lhncpcx.market.alicloudapi.com/agricultural/products/query
method: POST
headers:
- key: Content-Type
value: application/x-www-form-urlencoded
- key: Authorization
value: APPCODE {{.config.appCode}}
- key: X-Ca-Nonce
value: '{{uuidv4}}'
responseTemplate:
prependBody: |+
# API Response Information
Below is the response from an API call. To help you understand the data, I've provided:
1. A detailed description of all fields in the response structure
2. The complete API response
## Response Structure
> Content-Type: application/json
- **code**: 接口返回码【注意不等于HTTP响应状态码】 (Type: integer)
- **data**: (Type: array)
- **data[].code**: 农产品代码 (Type: string)
- **data[].genus**: 产品种类 (Type: string)
- **data[].genusCode**: 产品种类代码 (Type: string)
- **data[].name**: 名称 (Type: string)
- **msg**: 接口返回码对应的描述信息 (Type: string)
- **taskNo**: 任务订单号【可反馈服务商复核对应订单】 (Type: string)
## Original Response
- name: newest-price
description: 最新参考价
args:
- name: city
description: 地级市名称
type: string
position: body
- name: code
description: 农产品代码通过【支持产品查询】接口获取的code
type: string
required: true
position: body
- name: province
description: 省份名称,暂不支持港澳台地区,省份名字不带“省”字,譬如:浙江省,输入浙江
type: string
required: true
position: body
requestTemplate:
url: https://lhncpcx.market.alicloudapi.com/agricultural/products/lastest/reference-price
method: POST
headers:
- key: Content-Type
value: application/x-www-form-urlencoded
- key: Authorization
value: APPCODE {{.config.appCode}}
- key: X-Ca-Nonce
value: '{{uuidv4}}'
responseTemplate:
prependBody: |+
# API Response Information
Below is the response from an API call. To help you understand the data, I've provided:
1. A detailed description of all fields in the response structure
2. The complete API response
## Response Structure
> Content-Type: application/json
- **code**: 接口返回码【注意不等于HTTP响应状态码】 (Type: integer)
- **data**: (Type: array)
- **data[].address**: 价格获取地址(单位) (Type: string)
- **data[].date**: 更新时间 (Type: string)
- **data[].money**: 价格 (Type: string)
- **data[].unit**: 单位 (Type: string)
- **msg**: 接口返回码对应的描述信息 (Type: string)
- **taskNo**: 任务订单号【可反馈服务商复核对应订单】 (Type: string)
## Original Response

View File

@@ -0,0 +1,37 @@
# Book Query
The APP Code required for API authentication can be applied for at the Alibaba Cloud API Marketplace: https://market.aliyun.com/apimarket/detail/cmapi00066353
## Overview
The `book-query` service is primarily used to query detailed information about books using their ISBN numbers. This service accepts a request containing an ISBN number and sends a request to an external API to retrieve all available data related to that ISBN, including but not limited to the author, publication date, and publisher. This feature is very useful for library management systems, online bookstores, and other applications that need to quickly look up book details based on ISBN.
## Tool Introduction
### ISBN Number Query
- **Purpose**: This tool allows users to input an ISBN number and obtain comprehensive information about the corresponding book.
- **Use Cases**:
- When developers or system integrators need to provide users with a book search function based on ISBN.
- For those who want to quickly learn all relevant information about a book (such as author, edition, price, etc.) through its ISBN.
- As a basic data query method when building platforms that involve managing or selling a large number of books.
#### Parameter Description
- `isbn`: The ISBN number provided by the user, which is a string. This is the only required piece of information in the query process, used to locate a specific book record.
#### Request Example
- **URL**: https://lhisbnshcx.market.alicloudapi.com/isbn/query
- **Method**: POST
- **Headers**:
- Content-Type: application/x-www-form-urlencoded
- Authorization: Use the APP CODE for authentication
- X-Ca-Nonce: A generated random UUID value to ensure the uniqueness of each request
#### Response Structure
The response will be returned in JSON format and will include the following main fields:
- `code`: The status code returned by the interface, different from the HTTP status code.
- `data`: An object containing specific book information.
- `details[]`: An array of specific book details, where each element represents a record and includes various attributes such as author, title, and publisher.
- `msg`: A description message corresponding to the returned status code.
- `taskNo`: The task order number, which can be used for subsequent service provider verification.
This is a brief overview of the MCP server and its components mentioned in the YAML configuration file. Through these tools and services, effective querying and management of book information can be achieved.

View File

@@ -0,0 +1,48 @@
# 图书查询
API认证需要的APP Code请在阿里云API市场申请: https://market.aliyun.com/apimarket/detail/cmapi00066353
## 什么是云市场API MCP服务
阿里云云市场是生态伙伴的交易服务平台我们致力于为合作伙伴提供覆盖上云、商业化和售卖的全链路服务帮助客户高效获取、部署和管理优质生态产品。云市场的API服务涵盖以下几个类目应用开发、身份验证与金融、车辆交通与物流、企业服务、短信与运营商、AI应用与OCR、生活服务。
云市场API依托Higress提供MCP服务您只需在云市场完成订阅并获取AppCode通过Higress MCP Server进行配置即可无缝集成云市场API服务。
## 如何在使用云市场API MCP服务
1. 进入API详情页订阅该API。您可以优先使用免费试用。
2. 前往云市场用户控制台使用阿里云账号登陆后查看已订阅API服务的AppCode并配置到Higress MCP Server的配置中。注意在阿里云市场订阅API服务后您将获得AppCode。对于您订阅的所有API服务此AppCode是相同的您只需使用这一个AppCode即可访问所有已订阅的API服务。
3. 云市场用户控制台会实时展示已订阅的预付费API服务的可用额度如您免费试用额度已用完您可以选择重新订阅。
## 功能简介
主要用于通过ISBN书号来查询图书的详细信息。该服务接收一个包含ISBN书号的请求并向外部API发送请求以获取与该ISBN相关的所有可用数据包括但不限于作者、出版日期、出版社等信息。此功能对于图书馆管理系统、在线书店以及其他需要根据ISBN快速查找书籍详情的应用非常有用。
## 工具简介
### ISBN书号查询
- **用途**此工具允许用户输入ISBN书号并获取关于该书号所对应书籍的全面信息。
- **使用场景**
- 当开发者或系统集成者需要为用户提供基于ISBN的书籍检索功能时。
- 对于那些希望通过ISBN快速了解一本书籍的所有相关信息如作者、版本、价格等的情况。
- 在构建涉及大量书籍管理或者销售的平台时作为基础的数据查询手段之一。
#### 参数说明
- `isbn`: 用户必须提供的ISBN书号类型为字符串。这是查询过程中唯一必需的信息点用于定位特定的图书记录。
#### 请求示例
- **URL**: https://lhisbnshcx.market.alicloudapi.com/isbn/query
- **方法**: POST
- **头部信息**:
- Content-Type: application/x-www-form-urlencoded
- Authorization: 使用APP CODE进行身份验证
- X-Ca-Nonce: 生成的随机UUID值确保每次请求的独特性
#### 响应结构
响应将以JSON格式返回并包含以下主要字段
- `code`: 接口返回的状态码不同于HTTP状态码。
- `data`: 包含具体图书信息的对象。
- `details[]`: 图书的具体细节数组,每个元素代表一条记录,其中包括了诸如作者、标题、出版社等多种属性。
- `msg`: 返回的状态码对应的描述信息。
- `taskNo`: 任务订单号,可用于后续的服务商复核。
以上是针对YAML配置文件中提到的MCP服务器及其组件的一个简要概述。通过这些工具和服务可以有效地实现对图书信息的查询和管理。

View File

@@ -0,0 +1,191 @@
{
"info": {
"description": "【感受科技的温度】ISBN标准书号查询-ISBN书号查询-ISBN图书查询-ISBN图书详情信息查询-图书编号查询 —— 输入ISBN书号查询图书详情信息返回包含书名、作者、出版社、价格、出版日期、印次、装帧方式、语种、摘要等详细图书信息。【怜花数科】",
"title": "ISBN标准书号查询-ISBN书号查询-ISBN图书查询-ISBN图书详情信息查询-图书编号查询",
"version": "1.0.0"
},
"openapi": "3.0.1",
"paths": {
"/isbn/query": {
"post": {
"operationId": "ISBN书号查询",
"summary": "ISBN书号查询",
"requestBody": {
"content": {
"application/x-www-form-urlencoded": {
"schema": {
"type": "object",
"properties": {
"isbn": {
"description": "ISBN书号",
"type": "string"
}
},
"required": [
"isbn"
]
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"description": "接口返回码【注意不等于HTTP响应状态码】"
},
"msg": {
"type": "string",
"description": "接口返回码对应的描述信息"
},
"taskNo": {
"type": "string",
"description": "任务订单号【可反馈服务商复核对应订单】"
},
"data": {
"type": "object",
"properties": {
"details": {
"type": "array",
"items": {
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "书名"
},
"author": {
"type": "string",
"description": "作者、编者、译者信息"
},
"publisher": {
"type": "string",
"description": "出版社"
},
"pubDate": {
"type": "string",
"description": "出版日期"
},
"pubPlace": {
"type": "string",
"description": "出版地"
},
"isbn": {
"type": "string",
"description": "13位isbn号"
},
"isbn10": {
"type": "string",
"description": "10位isbn号"
},
"price": {
"type": "string",
"description": "定价"
},
"genus": {
"type": "string",
"description": "中图分类号"
},
"levelNum": {
"type": "string",
"description": "读者评分"
},
"heatNum": {
"type": "string",
"description": "图书热度"
},
"format": {
"type": "string",
"description": "纸张开数"
},
"binding": {
"type": "string",
"description": "装帧信息"
},
"page": {
"type": "string",
"description": "页数"
},
"wordNum": {
"type": "string",
"description": "字数"
},
"edition": {
"type": "string",
"description": "版次"
},
"yinci": {
"type": "string",
"description": "印次"
},
"paper": {
"type": "string",
"description": "书籍纸张类型"
},
"language": {
"type": "string",
"description": "语言"
},
"keyword": {
"type": "string",
"description": "图书关键词"
},
"img": {
"type": "string",
"description": "封面链接【提示图片链接24小时有效超过失效不可访问】"
},
"bookCatalog": {
"type": "string",
"description": "目录"
},
"gist": {
"type": "string",
"description": "图书内容简介"
},
"cipTxt": {
"type": "string",
"description": "cip信息"
},
"annotation": {
"type": "string",
"description": "附注"
},
"subject": {
"type": "string",
"description": "主题"
},
"series": {
"type": "string",
"description": "丛书信息,非丛书为空"
},
"batch": {
"type": "string",
"description": "丛编信息"
}
}
}
}
}
}
}
}
}
},
"description": "成功"
}
}
}
}
},
"servers": [
{
"url": "https://lhisbnshcx.market.alicloudapi.com"
}
]
}

View File

@@ -0,0 +1,72 @@
server:
name: book-query
config:
appCode: ""
tools:
- name: isbn-query
description: ISBN书号查询
args:
- name: isbn
description: ISBN书号
type: string
required: true
position: body
requestTemplate:
url: https://lhisbnshcx.market.alicloudapi.com/isbn/query
method: POST
headers:
- key: Content-Type
value: application/x-www-form-urlencoded
- key: Authorization
value: APPCODE {{.config.appCode}}
- key: X-Ca-Nonce
value: '{{uuidv4}}'
responseTemplate:
prependBody: |+
# API Response Information
Below is the response from an API call. To help you understand the data, I've provided:
1. A detailed description of all fields in the response structure
2. The complete API response
## Response Structure
> Content-Type: application/json
- **code**: 接口返回码【注意不等于HTTP响应状态码】 (Type: integer)
- **data**: (Type: object)
- **data.details**: (Type: array)
- **data.details[].annotation**: 附注 (Type: string)
- **data.details[].author**: 作者、编者、译者信息 (Type: string)
- **data.details[].batch**: 丛编信息 (Type: string)
- **data.details[].binding**: 装帧信息 (Type: string)
- **data.details[].bookCatalog**: 目录 (Type: string)
- **data.details[].cipTxt**: cip信息 (Type: string)
- **data.details[].edition**: 版次 (Type: string)
- **data.details[].format**: 纸张开数 (Type: string)
- **data.details[].genus**: 中图分类号 (Type: string)
- **data.details[].gist**: 图书内容简介 (Type: string)
- **data.details[].heatNum**: 图书热度 (Type: string)
- **data.details[].img**: 封面链接【提示图片链接24小时有效超过失效不可访问】 (Type: string)
- **data.details[].isbn**: 13位isbn号 (Type: string)
- **data.details[].isbn10**: 10位isbn号 (Type: string)
- **data.details[].keyword**: 图书关键词 (Type: string)
- **data.details[].language**: 语言 (Type: string)
- **data.details[].levelNum**: 读者评分 (Type: string)
- **data.details[].page**: 页数 (Type: string)
- **data.details[].paper**: 书籍纸张类型 (Type: string)
- **data.details[].price**: 定价 (Type: string)
- **data.details[].pubDate**: 出版日期 (Type: string)
- **data.details[].pubPlace**: 出版地 (Type: string)
- **data.details[].publisher**: 出版社 (Type: string)
- **data.details[].series**: 丛书信息,非丛书为空 (Type: string)
- **data.details[].subject**: 主题 (Type: string)
- **data.details[].title**: 书名 (Type: string)
- **data.details[].wordNum**: 字数 (Type: string)
- **data.details[].yinci**: 印次 (Type: string)
- **msg**: 接口返回码对应的描述信息 (Type: string)
- **taskNo**: 任务订单号【可反馈服务商复核对应订单】 (Type: string)
## Original Response

View File

@@ -0,0 +1,34 @@
# Brave Search MCP Server
An MCP server implementation that integrates the Brave Search API, providing web and local search capabilities.
## Features
- **Web Search**: Supports general queries, news, articles, with pagination and time control
- **Local Search**: Find businesses, restaurants, and services with detailed information
Source code: [https://github.com/modelcontextprotocol/servers/tree/main/src/brave-search](https://github.com/modelcontextprotocol/servers/tree/main/src/brave-search)
# Usage Guide
## Get API-KEY
1. Register for a Brave Search API account [Visit official website](https://brave.com/search/api/)
2. Choose a plan (free plan includes 2000 queries per month)
3. Generate API key through developer console [Go to console](https://api.search.brave.com/app/keys)
## Generate SSE URL
On the MCP Server interface, log in and enter the API-KEY to generate the URL.
## Configure MCP Client
On the user's MCP Client interface, add the generated SSE URL to the MCP Server list.
```json
"mcpServers": {
"bravesearch": {
"url": "https://mcp.higress.ai/mcp-bravesearch/{generate_key}",
}
}
```

View File

@@ -0,0 +1,38 @@
# Brave Search MCP Server
一个集成Brave搜索API的MCP服务器实现提供网页和本地搜索功能。
## 功能
- **网页搜索**:支持通用查询、新闻、文章,具备分页和时效性控制
- **本地搜索**:查找带有详细信息的企业、餐厅和服务
源码地址:[https://github.com/modelcontextprotocol/servers/tree/main/src/brave-search](https://github.com/modelcontextprotocol/servers/tree/main/src/brave-search)
# 使用教程
## 获取 API-KEY
1. 注册Brave搜索API账号 [访问官网](https://brave.com/search/api/)
2. 选择套餐免费套餐每月包含2000次查询
3. 通过开发者控制台生成 API 密钥 [前往控制台](https://api.search.brave.com/app/keys)
## 生成 SSE URL
在 MCP Server 界面,登录后输入 API-KEY生成URL。
## 配置 MCP Client
在用户的 MCP Client 界面,将生成的 SSE URL添加到MCP Server列表中。
```json
"mcpServers": {
"bravesearch": {
"url": "https://mcp.higress.ai/mcp-bravesearch/{generate_key}",
}
}
```

View File

@@ -0,0 +1,121 @@
server:
name: brave-search-server
config:
apiKey: ""
tools:
- name: brave_web_search
description: "使用Brave Search API进行网页搜索适用于一般查询、新闻、文章和在线内容。支持分页、内容过滤和新鲜度控制。"
args:
- name: q
description: "搜索查询最多400字符50个词"
type: string
required: true
- name: count
description: "结果数量1-20默认10"
type: integer
required: false
default: 10
- name: offset
description: "分页偏移量最大9默认0"
type: integer
required: false
default: 0
- name: search_lang
description: "搜索语言"
type: string
required: false
enum: ["en", "zh-hans"]
requestTemplate:
url: "https://api.search.brave.com/res/v1/web/search"
method: GET
argsToUrlParam: true
headers:
- key: Accept
value: "application/json"
- key: X-Subscription-Token
value: "{{.config.apiKey}}"
responseTemplate:
body: |
{{- range $index, $item := .web.results }}
## 结果 {{add $index 1}}
- **标题**: {{ $item.title }}
- **描述**: {{ $item.description }}
- **URL**: {{ $item.url }}
{{- end }}
{{- if .locations.results }}
{{- range $index, $item := .locations.results }}
## 结果 {{add $index 1}}
- **locationID**: {{ $item.id }}
{{- end }}
{{- end }}
- name: brave_local_search_pois
description: "使用Brave Local Search API搜索本地POI兴趣点信息包括名称、地址、电话、评分等信息。"
args:
- name: ids
description: "Location ID列表通过brave_web_search获取"
type: array
required: true
- name: search_lang
description: "搜索语言"
type: string
required: false
default: "en"
- name: search_lang
description: "响应语言"
type: string
required: false
default: "en-US"
requestTemplate:
url: "https://api.search.brave.com/res/v1/local/pois"
method: GET
argsToUrlParam: true
headers:
- key: Accept
value: "application/json"
- key: X-Subscription-Token
value: "{{.config.apiKey}}"
responseTemplate:
body: |
{{- range $index, $item := .results }}
## POI {{add $index 1}}
- **名称**: {{ $item.name }}
- **地址**: {{ $item.address.streetAddress }}, {{ $item.address.addressLocality }}, {{ $item.address.addressRegion }} {{ $item.address.postalCode }}
- **电话**: {{ $item.phone }}
- **评分**: {{ $item.rating.ratingValue }} ({{ $item.rating.ratingCount }} 条评价)
- **价格范围**: {{ $item.priceRange }}
- **营业时间**: {{ join $item.openingHours ", " }}
{{- end }}
- name: brave_local_search_descriptions
description: "使用Brave Local Search API获取本地POI的描述信息。"
args:
- name: ids
description: "Location ID列表通过brave_web_search获取"
type: array
required: true
- name: search_lang
description: "搜索语言"
type: string
required: false
default: "en"
- name: search_lang
description: "响应语言"
type: string
required: false
default: "en-US"
requestTemplate:
url: "https://api.search.brave.com/res/v1/local/descriptions"
method: GET
argsToUrlParam: true
headers:
- key: Accept
value: "application/json"
- key: X-Subscription-Token
value: "{{.config.apiKey}}"
responseTemplate:
body: |
{{- range $id, $desc := .descriptions }}
## 描述 {{ $id }}
{{ $desc }}
{{- end }}

View File

@@ -0,0 +1,45 @@
# Enterprise Credit Rating
The APP Code required for API authentication can be applied for on the Alibaba Cloud API Marketplace: https://market.aliyun.com/apimarket/detail/cmapi00067564
# MCP Server Configuration Overview
## Function Overview
This MCP server is primarily used to handle query requests related to enterprise credit ratings. By interacting with specific APIs available on the Alibaba Cloud Marketplace, this service can return detailed credit rating information of a company based on provided information such as the company name, registration number, or social credit code. This allows users to conveniently obtain the latest credit status of target companies, including but not limited to bond credit ratings, entity ratings, and rating outlooks.
## Tool Overview
### Enterprise Credit Rating
- **Purpose**: Provides an interface for querying the credit rating information of specified enterprises.
- **Use Cases**: Suitable for scenarios where a comprehensive understanding of an enterprise's credit status is needed, such as when financial institutions decide whether to provide loans to a company; or when suppliers investigate the creditworthiness of potential clients before choosing partners.
#### Parameter Description
- **keyword** (Required): The search keyword, which can be the company name, registration number, or social credit code.
- **pageNum**: The page number in the request pagination, defaulting to 1.
- **pageSize**: The number of result items per page, with a default value of 10.
#### Request Template
- **URL**: `https://slyhonour.market.alicloudapi.com/credit/rating`
- **Method**: GET
- **Headers**:
- Authorization: Use the application code as the authentication method
- X-Ca-Nonce: A unique identifier generated automatically
#### Response Structure
- **code**: Status code
- **data**:
- **items[]**:
- alias: Rating company alias
- bondCreditLevel: Bond credit level
- gid: Global ID
- logo: Rating company logo
- ratingCompanyName: Rating company name
- ratingDate: Rating date
- ratingOutlook: Rating outlook
- subjectLevel: Subject level
- orderNo: Order number
- total: Total number of records
- **msg**: Message content returned
- **success**: Boolean flag indicating whether the operation was successful
This tool provides detailed enterprise credit assessment data, helping users to quickly and accurately evaluate a company's financial health and its ability to repay debts.

View File

@@ -0,0 +1,56 @@
# 企业信用评级
API认证需要的APP Code请在阿里云API市场申请: https://market.aliyun.com/apimarket/detail/cmapi00067564
## 什么是云市场API MCP服务
阿里云云市场是生态伙伴的交易服务平台我们致力于为合作伙伴提供覆盖上云、商业化和售卖的全链路服务帮助客户高效获取、部署和管理优质生态产品。云市场的API服务涵盖以下几个类目应用开发、身份验证与金融、车辆交通与物流、企业服务、短信与运营商、AI应用与OCR、生活服务。
云市场API依托Higress提供MCP服务您只需在云市场完成订阅并获取AppCode通过Higress MCP Server进行配置即可无缝集成云市场API服务。
## 如何在使用云市场API MCP服务
1. 进入API详情页订阅该API。您可以优先使用免费试用。
2. 前往云市场用户控制台使用阿里云账号登陆后查看已订阅API服务的AppCode并配置到Higress MCP Server的配置中。注意在阿里云市场订阅API服务后您将获得AppCode。对于您订阅的所有API服务此AppCode是相同的您只需使用这一个AppCode即可访问所有已订阅的API服务。
3. 云市场用户控制台会实时展示已订阅的预付费API服务的可用额度如您免费试用额度已用完您可以选择重新订阅。
# MCP服务器配置功能简介
## 功能简介
主要用于处理企业信用评级相关的查询请求。通过与阿里云市场上的特定API进行交互该服务能够根据提供的公司名称、注册号或社会统一信用代码等信息返回对应企业的信用评级详情。这使得用户可以方便地获取到目标公司的最新信用状况包括但不限于债券信用等级、主体等级以及评级展望等内容。
## 工具简介
### 企业信用评级
- **用途**:提供一个接口用于查询指定企业的信用评级信息。
- **使用场景**:适用于需要对企业信用状况进行全面了解的情况,比如金融机构在决定是否向某企业提供贷款时;供应商在选择合作伙伴前对潜在客户的资信进行调查等。
#### 参数说明
- **keyword** (必填): 搜索关键字,可以是公司名称、注册号或者社会统一信用代码。
- **pageNum**: 请求分页中的页码数默认从1开始计数。
- **pageSize**: 每页显示的结果条目数默认值为10。
#### 请求模板
- **URL**: `https://slyhonour.market.alicloudapi.com/credit/rating`
- **方法**: GET
- **头部信息**:
- Authorization: 使用应用程序编码作为认证方式
- X-Ca-Nonce: 自动生成的唯一标识符
#### 响应结构
- **code**: 状态码
- **data**:
- **items[]**:
- alias: 评级公司别名
- bondCreditLevel: 债券信用等级
- gid: 全球ID
- logo: 评级公司Logo
- ratingCompanyName: 评级公司名称
- ratingDate: 评级日期
- ratingOutlook: 评级展望
- subjectLevel: 主体等级
- orderNo: 订单号
- total: 总记录数
- **msg**: 返回的消息内容
- **success**: 操作是否成功的布尔标志
此工具提供了详尽的企业信用评估数据,有助于用户快速准确地判断一家企业的财务健康状况及其偿还债务的能力。

View File

@@ -0,0 +1,143 @@
{
"info": {
"description": "【企业信用评级-企业信用查询】★通过公司名称、注册号或社会统一信用代码任一项查询企业信用评级信息。★毫秒级响应支持高并发24h不间断运维专业技术支持在线服务。★新老客户享专属活动价详情可咨询客服。——全品类接口专家",
"title": "企业信用评级-企业信用查询【数链云】",
"version": "1.0.0"
},
"openapi": "3.0.1",
"paths": {
"/credit/rating": {
"get": {
"operationId": "企业信用评级",
"summary": "企业信用评级",
"parameters": [
{
"description": "搜索关键字(公司名称、注册号或社会统一信用代码)",
"in": "query",
"name": "keyword",
"required": true,
"schema": {
"type": "string"
}
},
{
"description": "分页数量 1开始",
"in": "query",
"name": "pageNum",
"required": false,
"schema": {
"type": "string"
}
},
{
"description": "每页数量 默认 10",
"in": "query",
"name": "pageSize",
"required": false,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"properties": {
"msg": {
"description": "响应消息",
"example": "成功",
"type": "string"
},
"success": {
"description": "是否成功",
"example": "true",
"type": "boolean"
},
"code": {
"description": "状态码",
"example": "200",
"type": "integer"
},
"data": {
"properties": {
"orderNo": {
"description": "订单号",
"example": "276085547371344356",
"type": "string"
},
"total": {
"description": "总数",
"example": "22",
"type": "integer"
},
"items": {
"items": {
"properties": {
"ratingOutlook": {
"description": "评级展望",
"example": "负面",
"type": "string"
},
"ratingDate": {
"description": "评级日期",
"example": "2024-04-16",
"format": "date",
"type": "string"
},
"gid": {
"description": "全球ID",
"nullable": true,
"type": "string"
},
"ratingCompanyName": {
"description": "评级公司名称",
"example": "惠誉国际信用评级有限公司",
"type": "string"
},
"logo": {
"description": "评级公司Logo",
"nullable": true,
"type": "string"
},
"alias": {
"description": "评级公司别名",
"example": "惠誉国际",
"type": "string"
},
"bondCreditLevel": {
"description": "债券信用等级",
"nullable": true,
"type": "string"
},
"subjectLevel": {
"description": "主体等级",
"example": "A+",
"type": "string"
}
},
"type": "object"
},
"type": "array"
}
},
"type": "object"
}
},
"type": "object"
}
}
},
"description": "成功响应"
}
}
}
}
},
"servers": [
{
"url": "https://slyhonour.market.alicloudapi.com"
}
]
}

View File

@@ -0,0 +1,60 @@
server:
name: business-credit-rating
config:
appCode: ""
tools:
- name: bussiness-credit-rating
description: 企业信用评级
args:
- name: keyword
description: 搜索关键字(公司名称、注册号或社会统一信用代码)
type: string
required: true
position: query
- name: pageNum
description: 分页数量 1开始
type: string
position: query
- name: pageSize
description: 每页数量 默认 10
type: string
position: query
requestTemplate:
url: https://slyhonour.market.alicloudapi.com/credit/rating
method: GET
headers:
- key: Authorization
value: APPCODE {{.config.appCode}}
- key: X-Ca-Nonce
value: '{{uuidv4}}'
responseTemplate:
prependBody: |+
# API Response Information
Below is the response from an API call. To help you understand the data, I've provided:
1. A detailed description of all fields in the response structure
2. The complete API response
## Response Structure
> Content-Type: application/json
- **code**: 状态码 (Type: integer)
- **data**: (Type: object)
- **data.items**: (Type: array)
- **data.items[].alias**: 评级公司别名 (Type: string)
- **data.items[].bondCreditLevel**: 债券信用等级 (Type: string)
- **data.items[].gid**: 全球ID (Type: string)
- **data.items[].logo**: 评级公司Logo (Type: string)
- **data.items[].ratingCompanyName**: 评级公司名称 (Type: string)
- **data.items[].ratingDate**: 评级日期 (Type: string)
- **data.items[].ratingOutlook**: 评级展望 (Type: string)
- **data.items[].subjectLevel**: 主体等级 (Type: string)
- **data.orderNo**: 订单号 (Type: string)
- **data.total**: 总数 (Type: integer)
- **msg**: 响应消息 (Type: string)
- **success**: 是否成功 (Type: boolean)
## Original Response

View File

@@ -0,0 +1,78 @@
# Business Information Inquiry
The APP Code required for API authentication can be applied for on the Alibaba Cloud API Marketplace: https://market.aliyun.com/apimarket/detail/cmapi029030
# MCP Server Function Overview Document
## Function Overview
The MCP server is primarily used to provide query services for enterprise-related information. Through a series of API interfaces, users can obtain detailed information including but not limited to patent information, copyright information, branch information, and business registration data of enterprises. These tools are designed to help businesses better understand their own or other companies' operational status, legal risks, and market performance.
## Tool Introduction
### 1. Enterprise Patent Information
- **Purpose**: This tool is used to query all publicly available patent information of a specified company, covering various types such as inventions, utility models, and design patents.
- **Use Case**: When evaluating a company's innovation capability or intellectual property layout, this tool can provide relevant data to support decision-making.
### 2. Other Copyright Information of Enterprises
- **Purpose**: In addition to patents, it also provides a service for querying other types of copyright information of enterprises.
- **Use Case**: Suitable for users interested in copyright protection in specific fields, such as copyright agencies or research institutions.
### 3. Enterprise Branch Information
- **Purpose**: Lists all subsidiary or branch details under a given company name.
- **Use Case**: Very useful for corporate analysts who want to fully understand the structure of a group.
### 4. Enterprise Name Search Suggestion Query
- **Purpose**: Returns a list of related company names based on the input keywords, suitable for associative searches.
- **Use Case**: Quickly find target companies during preliminary market research.
### 5. Enterprise Trademark Information
- **Purpose**: Displays the trademarks held by an enterprise and their status.
- **Use Case**: Brand management teams can use this service to monitor competitors' brand activities.
### 6. Enterprise External Investment Information
- **Purpose**: Provides specific details about an enterprise's external investment projects.
- **Use Case**: Investors and financial advisors can use this to understand the capital operations of a company.
### 7. Fuzzy Search for Enterprise Business Registration Data
- **Purpose**: Finds basic information of enterprises based on incomplete matching conditions (e.g., abbreviations).
- **Use Case**: Useful for quickly locating a target company when only partial information is known.
### 8. Precise Search for Enterprise Business Registration Data
- **Purpose**: Conducts precise queries based on the full company name or social credit code.
- **Use Case**: Suitable for situations where accurate and error-free business registration information is needed.
### 9. Enterprise Annual Report Information
- **Purpose**: Views various financial indicators and other important information included in the annual report of an enterprise.
- **Use Case**: Provides a basis for investors to analyze the financial health of a company.
### 10. Enterprise Recruitment Information
- **Purpose**: Collects and displays job vacancies and related requirements posted by enterprises.
- **Use Case**: Job seekers looking for opportunities; HR departments understanding industry talent demand trends.
### 11. Enterprise Legal Litigation Information
- **Purpose**: Obtains details of legal disputes involving the enterprise.
- **Use Case**: Legal advisors assessing the risk level of potential partners.
### 12. Enterprise Court Announcement Information
- **Purpose**: Reviews the content of court announcements related to the enterprise.
- **Use Case**: Tracking judicial dynamics of specific enterprises.
### 13. Enterprise Abnormal Operation Information
- **Purpose**: Reveals records of issues that have occurred during the operation of the enterprise.
- **Use Case**: Regulatory bodies overseeing corporate compliance; consumer protection organizations safeguarding public interests.
### 14. Enterprise Financing Information
- **Purpose**: Tracks the specifics of each financing event of the enterprise.
- **Use Case**: Startups monitoring the fundraising activities of competitors in the same industry.
### 15. Enterprise Executed Party Information
- **Purpose**: Discloses the list of enterprises as executed parties and related case information.
- **Use Case**: Financial institutions assessing the creditworthiness of loan applicants.
### 16. Enterprise Software Copyright Information
- **Purpose**: Lists the software works owned by the enterprise and their copyright status.
- **Use Case**: IT professionals understanding the technical strength of peers.
### 17. Big Data Enterprise Profile Tag Information
- **Purpose**: A collection of enterprise characteristic tags generated through big data analysis.
- **Use Case**: Marketing personnel customizing personalized promotion strategies; academic researchers conducting studies on enterprise behavior patterns.

View File

@@ -0,0 +1,89 @@
# 工商信息查询
API认证需要的APP Code请在阿里云API市场申请: https://market.aliyun.com/apimarket/detail/cmapi029030
## 什么是云市场API MCP服务
阿里云云市场是生态伙伴的交易服务平台我们致力于为合作伙伴提供覆盖上云、商业化和售卖的全链路服务帮助客户高效获取、部署和管理优质生态产品。云市场的API服务涵盖以下几个类目应用开发、身份验证与金融、车辆交通与物流、企业服务、短信与运营商、AI应用与OCR、生活服务。
云市场API依托Higress提供MCP服务您只需在云市场完成订阅并获取AppCode通过Higress MCP Server进行配置即可无缝集成云市场API服务。
## 如何在使用云市场API MCP服务
1. 进入API详情页订阅该API。您可以优先使用免费试用。
2. 前往云市场用户控制台使用阿里云账号登陆后查看已订阅API服务的AppCode并配置到Higress MCP Server的配置中。注意在阿里云市场订阅API服务后您将获得AppCode。对于您订阅的所有API服务此AppCode是相同的您只需使用这一个AppCode即可访问所有已订阅的API服务。
3. 云市场用户控制台会实时展示已订阅的预付费API服务的可用额度如您免费试用额度已用完您可以选择重新订阅。
# MCP服务器功能简介文档
## 功能简介
主要用于提供企业相关信息的查询服务。通过一系列API接口用户可以获取到包括但不限于企业的专利信息、著作权信息、分支机构信息、工商数据等详细资料。这些工具旨在帮助企业更好地了解其自身或其他企业的经营状况、法律风险及市场表现等方面的信息。
## 工具简介
### 1. 企业专利信息
- **用途**:此工具用于查询指定企业的所有公开专利信息,涵盖发明、实用新型、外观设计等多种类型。
- **使用场景**:当需要评估一家企业的创新能力或知识产权布局时,可以通过该工具获取相关数据支持决策制定。
### 2. 企业其它著作权信息
- **用途**:除了专利之外,还提供了关于企业其他类型的著作权信息查询服务。
- **使用场景**:适用于对特定领域内版权保护情况感兴趣的用户,如版权代理机构或者研究机构。
### 3. 企业分支机构信息
- **用途**:能够列出给定公司名下的所有子公司或分公司详情。
- **使用场景**:对于希望全面了解某集团架构的企业分析师来说非常有用。
### 4. 企业名称搜索建议查询
- **用途**:根据输入的关键字返回相关的公司名称列表,适合做联想式搜索。
- **使用场景**:在进行初步市场调研时快速找到目标企业。
### 5. 企业商标信息
- **用途**:显示企业所持有的商标及其状态等信息。
- **使用场景**:品牌管理团队可利用这项服务来监控竞争对手的品牌活动。
### 6. 企业对外投资信息
- **用途**:提供有关企业对外投资项目的具体细节。
- **使用场景**:投资者和财务顾问可通过此途径了解企业的资本运作情况。
### 7. 企业工商数据模糊查询
- **用途**:基于不完全匹配条件(如简称)查找符合条件的企业基本信息。
- **使用场景**:当只知道部分企业信息时,可用于快速定位目标企业。
### 8. 企业工商数据精准查询
- **用途**:针对已知完整企业名称或社会信用代码进行精确查询。
- **使用场景**:适用于需要获取准确无误的企业注册信息的情况。
### 9. 企业年报信息
- **用途**:查看企业年度报告中包含的各项财务指标及其他重要信息。
- **使用场景**:为投资者分析企业财务健康状况提供依据。
### 10. 企业招聘信息
- **用途**:收集并展示企业发布的职位空缺及相关要求。
- **使用场景**:求职者寻找工作机会;人力资源部门了解行业人才需求趋势。
### 11. 企业法律诉讼信息
- **用途**:获取涉及企业的法律纠纷案件详情。
- **使用场景**:法律顾问评估潜在合作伙伴的风险等级。
### 12. 企业法院公告信息
- **用途**:查阅与企业相关的法院公告内容。
- **使用场景**:跟踪特定企业的司法动态。
### 13. 企业经营异常信息
- **用途**:揭示企业在经营过程中出现的问题记录。
- **使用场景**:监管机构监督企业合规性;消费者保护组织维护公众利益。
### 14. 企业融资信息
- **用途**:追踪企业历次融资事件的具体情况。
- **使用场景**:创业公司关注同行业内竞争者的资金募集情况。
### 15. 企业被执行人信息
- **用途**:揭露作为被执行人的企业名单及相关案件信息。
- **使用场景**:金融机构评估贷款申请者的信用水平。
### 16. 企业软件著作权信息
- **用途**:列出企业拥有的软件作品及其版权状态。
- **使用场景**IT行业从业者了解同行技术实力。
### 17. 大数据企业画像标签信息
- **用途**:基于大数据分析生成的企业特征标签集合。
- **使用场景**:市场营销人员定制个性化推广策略;学术研究人员开展企业行为模式研究。

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,775 @@
server:
name: business-info-query
config:
appCode: ""
tools:
- name: business-patent-query
description: 查询企业公布的专利信息,包括发明专利,实用新型,实用外观,发明授权等类型
args:
- name: CompanyName
description: CompanyName
type: string
required: true
position: path
- name: isRaiseErrorCode
description: "当请求传入不存在企业名称时是否抛出404错误。0为否1为是默认为否。可以避免传入不存在企业时扣减次数。\t"
type: integer
position: query
requestTemplate:
url: https://api.81api.com/getCompanyPatentsInfo/{CompanyName}/
method: GET
headers:
- key: Authorization
value: APPCODE {{.config.appCode}}
- key: X-Ca-Nonce
value: '{{uuidv4}}'
responseTemplate:
prependBody: |+
# API Response Information
Below is the response from an API call. To help you understand the data, I've provided:
1. A detailed description of all fields in the response structure
2. The complete API response
## Response Structure
> Content-Type: application/json
- **data**: (Type: object)
- **data.list**: (Type: array)
- **data.list[].createDate**: 创建日期 (Type: string)
- **data.list[].createNum**: 专利编号 (Type: string)
- **data.list[].patentName**: 专利名称 (Type: string)
- **data.list[].type**: 专利类型 (Type: string)
- **data.total**: 总数 (Type: integer)
- **status**: 请求状态 (Type: boolean)
## Original Response
- name: business-other-copyright-query
description: 企业其它著作权信息
args:
- name: CompanyName
description: CompanyName
type: string
required: true
position: path
- name: isRaiseErrorCode
description: "当请求传入不存在企业名称时是否抛出404错误。0为否1为是默认为否。可以避免传入不存在企业时扣减次数。\t"
type: integer
position: query
requestTemplate:
url: https://api.81api.com/getCompanyOtherCopyrightsInfo/{CompanyName}/
method: GET
headers:
- key: Authorization
value: APPCODE {{.config.appCode}}
- key: X-Ca-Nonce
value: '{{uuidv4}}'
responseTemplate:
prependBody: |+
# API Response Information
Below is the response from an API call. To help you understand the data, I've provided:
1. A detailed description of all fields in the response structure
2. The complete API response
## Response Structure
> Content-Type: application/json
- **data**: (Type: object)
- **data.list**: (Type: array)
- **data.list[].className**: 类别名称 (Type: string)
- **data.list[].createDate**: 创建日期 (Type: string)
- **data.list[].name**: 名称 (Type: string)
- **data.list[].publishDate**: 发布日期 (Type: string)
- **data.list[].regNo**: 注册号 (Type: string)
- **data.total**: 总数量 (Type: integer)
- **status**: 状态标志,表示请求是否成功 (Type: boolean)
## Original Response
- name: business-branch-query
description: 企业所有分支机构信息
args:
- name: CompanyName
description: CompanyName
type: string
required: true
position: path
- name: PageNum
description: 查询页数,默认为第一页
type: integer
position: query
- name: isRaiseErrorCode
description: "当请求传入不存在企业名称时是否抛出404错误。0为否1为是默认为否。可以避免传入不存在企业时扣减次数。\t"
type: integer
position: query
requestTemplate:
url: https://api.81api.com/getCompanyBranchInfo/{CompanyName}/
method: GET
headers:
- key: Authorization
value: APPCODE {{.config.appCode}}
- key: X-Ca-Nonce
value: '{{uuidv4}}'
responseTemplate:
prependBody: |+
# API Response Information
Below is the response from an API call. To help you understand the data, I've provided:
1. A detailed description of all fields in the response structure
2. The complete API response
## Response Structure
> Content-Type: application/json
- **data**: (Type: object)
- **data.list**: (Type: array)
- **data.list[].name**: 分公司名称 (Type: string)
- **data.total**: 总数 (Type: integer)
- **status**: 状态标志,表示请求是否成功 (Type: boolean)
## Original Response
- name: business-name-query
description: 企业工商名称搜索建议查询,只返回推荐匹配的企业名称,适合联想查询,输入框搜索建议
args:
- name: Keyword
description: 查询关键字至少3个字
type: string
required: true
position: path
- name: isRaiseErrorCode
description: 当请求查询关键字无返回结果时是否抛出404错误。0为否1为是默认为否。可以避免传入无效关键字时扣减次数。
type: integer
position: query
requestTemplate:
url: https://api.81api.com/fuzzySuggestCompanyName/{Keyword}/
method: GET
headers:
- key: Authorization
value: APPCODE {{.config.appCode}}
- key: X-Ca-Nonce
value: '{{uuidv4}}'
responseTemplate:
prependBody: |+
# API Response Information
Below is the response from an API call. To help you understand the data, I've provided:
1. A detailed description of all fields in the response structure
2. The complete API response
## Response Structure
> Content-Type: application/json
- **data**: (Type: object)
- **data.list**: (Type: array)
- **data.list[].companyname**: 公司名称 (Type: string)
- **status**: 响应状态true表示成功 (Type: boolean)
## Original Response
- name: business-trademark-query
description: 企业商标信息
args:
- name: CompanyName
description: CompanyName
type: string
required: true
position: path
- name: isRaiseErrorCode
description: "当请求传入不存在企业名称时是否抛出404错误。0为否1为是默认为否。可以避免传入不存在企业时扣减次数。\t"
type: integer
position: query
requestTemplate:
url: https://api.81api.com/getCompanyTrademarksInfo/{CompanyName}/
method: GET
headers:
- key: Authorization
value: APPCODE {{.config.appCode}}
- key: X-Ca-Nonce
value: '{{uuidv4}}'
responseTemplate:
prependBody: |+
# API Response Information
Below is the response from an API call. To help you understand the data, I've provided:
1. A detailed description of all fields in the response structure
2. The complete API response
## Response Structure
> Content-Type: application/json
- **data**: (Type: object)
- **data.list**: (Type: array)
- **data.list[].name**: 名称 (Type: string)
- **data.list[].status**: 当前状态 (Type: string)
- **data.list[].type**: 类型 (Type: string)
- **data.total**: 总数 (Type: integer)
- **status**: 状态标志 (Type: boolean)
## Original Response
- name: busiiness-invest-query
description: 企业对外投资信息
args:
- name: CompanyName
description: CompanyName
type: string
required: true
position: path
- name: isRaiseErrorCode
description: "当请求传入不存在企业名称时是否抛出404错误。0为否1为是默认为否。可以避免传入不存在企业时扣减次数。\t"
type: integer
position: query
requestTemplate:
url: https://api.81api.com/getCompanyInvestEventsInfo/{CompanyName}/
method: GET
headers:
- key: Authorization
value: APPCODE {{.config.appCode}}
- key: X-Ca-Nonce
value: '{{uuidv4}}'
responseTemplate:
prependBody: |+
# API Response Information
Below is the response from an API call. To help you understand the data, I've provided:
1. A detailed description of all fields in the response structure
2. The complete API response
## Response Structure
> Content-Type: application/json
- **data**: (Type: object)
- **data.list**: (Type: array)
- **data.list[].investCapital**: 投资金额 (Type: string)
- **data.list[].investCompanyName**: 投资公司名称 (Type: string)
- **data.list[].investDate**: 投资日期 (Type: string)
- **data.total**: 列表总数 (Type: integer)
- **status**: 操作状态true表示成功 (Type: boolean)
## Original Response
- name: business-basic-query
description: 企业工商基本数据模糊查询
args:
- name: CompanyName
description: 支持企业名称、简称、注册号、信任号等模糊匹配
type: string
required: true
position: path
- name: PageNum
description: 查询页数,默认为第一页
type: integer
position: query
requestTemplate:
url: https://api.81api.com/fuzzyQueryCompanyInfo/{CompanyName}/
method: GET
headers:
- key: Authorization
value: APPCODE {{.config.appCode}}
- key: X-Ca-Nonce
value: '{{uuidv4}}'
responseTemplate:
prependBody: |+
# API Response Information
Below is the response from an API call. To help you understand the data, I've provided:
1. A detailed description of all fields in the response structure
2. The complete API response
## Response Structure
> Content-Type: application/json
- **data**: (Type: object)
- **data.list**: (Type: array)
- **data.list[].legal_person_name**: 法人代表姓名 (Type: string)
- **data.list[].name**: 公司名称 (Type: string)
- **data.list[].reg_capital**: 注册资本 (Type: string)
- **data.list[].reg_date**: 注册日期 (Type: string)
- **data.num**: 当前数量 (Type: integer)
- **data.total**: 总数 (Type: integer)
- **message**: 消息 (Type: string)
- **status**: 状态码 (Type: string)
## Original Response
- name: exact-business-query
description: 精准查询企业工商基本数据,包括工商注册信息,股东信息,变更记录,分支机构,董事会信息
args:
- name: CompanyNameOrCreditNo
description: 支持企业全称和企业社会信任代码
type: string
required: true
position: path
- name: isRaiseErrorCode
description: 当请求传入不存在企业名称时是否抛出404错误。0为否1为是默认为否。可以避免传入不存在企业时扣减次数。
type: integer
position: query
requestTemplate:
url: https://api.81api.com/getCompanyBaseInfo/{CompanyNameOrCreditNo}/
method: GET
headers:
- key: Authorization
value: APPCODE {{.config.appCode}}
- key: X-Ca-Nonce
value: '{{uuidv4}}'
responseTemplate:
prependBody: |+
# API Response Information
Below is the response from an API call. To help you understand the data, I've provided:
1. A detailed description of all fields in the response structure
2. The complete API response
## Response Structure
> Content-Type: application/json
- **data**: (Type: object)
- **data.changeRecordData**: (Type: object)
- **data.changeRecordData.hasMore**: 是否有更多变更记录 (Type: boolean)
- **data.changeRecordData.list**: (Type: array)
- **data.changeRecordData.list[].after**: 变更后内容 (Type: string)
- **data.changeRecordData.list[].before**: 变更前内容 (Type: string)
- **data.changeRecordData.list[].date**: 变更日期 (Type: string)
- **data.changeRecordData.list[].item**: 变更项目 (Type: string)
- **data.employeeData**: (Type: object)
- **data.employeeData.list**: (Type: array)
- **data.employeeData.list[].name**: 员工姓名 (Type: string)
- **data.employeeData.list[].title**: 职位 (Type: string)
- **data.employeeData.total**: 员工总数 (Type: integer)
- **data.legalPersonName**: 法定代表人姓名 (Type: string)
- **data.name**: 公司名称 (Type: string)
- **data.partnerData**: (Type: object)
- **data.partnerData.list**: (Type: array)
- **data.partnerData.list[].partnerName**: 股东姓名 (Type: string)
- **data.partnerData.list[].partnerType**: 股东类型 (Type: string)
- **data.partnerData.list[].totalRealCapital**: 实缴资本 (Type: string)
- **data.partnerData.list[].totalShouldCapital**: 认缴资本 (Type: string)
- **data.partnerData.total**: 股东总数 (Type: integer)
- **data.registerCapital**: 注册资本 (Type: string)
- **data.registerData**: (Type: object)
- **data.registerData.address**: 公司地址 (Type: string)
- **data.registerData.belongOrg**: 登记机关 (Type: string)
- **data.registerData.businessScope**: 经营范围 (Type: string)
- **data.registerData.businessTerm**: 营业期限 (Type: string)
- **data.registerData.creditNo**: 统一社会信用代码 (Type: string)
- **data.registerData.orgNo**: 组织机构代码 (Type: string)
- **data.registerData.regType**: 企业类型 (Type: string)
- **data.registerData.registerNo**: 工商注册号 (Type: string)
- **data.registerData.status**: 经营状态 (Type: string)
- **data.startDate**: 公司成立日期 (Type: string)
- **status**: 响应状态 (Type: boolean)
## Original Response
- name: business-year-report-query
description: 企业年报信息
args:
- name: CompanyName
description: CompanyName
type: string
required: true
position: path
- name: isRaiseErrorCode
description: "当请求传入不存在企业名称时是否抛出404错误。0为否1为是默认为否。可以避免传入不存在企业时扣减次数。\t"
type: integer
position: query
requestTemplate:
url: https://api.81api.com/getCompanyYearReportInfo/{CompanyName}/
method: GET
headers:
- key: Authorization
value: APPCODE {{.config.appCode}}
- key: X-Ca-Nonce
value: '{{uuidv4}}'
responseTemplate:
prependBody: |+
# API Response Information
Below is the response from an API call. To help you understand the data, I've provided:
1. A detailed description of all fields in the response structure
2. The complete API response
## Response Structure
> Content-Type: application/json
- **data**: (Type: array)
- **data[].rptDate**: 报告日期 (Type: string)
- **data[].rptDetail**: (Type: object)
- **data[].rptDetail.creditNo**: 统一社会信用代码 (Type: string)
- **data[].rptDetail.isEquity**: 是否有股权 (Type: string)
- **data[].rptDetail.isInvest**: 是否有投资 (Type: string)
- **data[].rptDetail.name**: 公司名称 (Type: string)
- **data[].rptDetail.staffNum**: 员工人数 (Type: string)
- **data[].rptDetail.status**: 公司状态 (Type: string)
- **data[].rptYear**: 报告年度 (Type: string)
- **status**: 响应状态 (Type: boolean)
## Original Response
- name: business-jobs-query
description: 企业招聘信息
args:
- name: CompanyName
description: CompanyName
type: string
required: true
position: path
- name: isRaiseErrorCode
description: "当请求传入不存在企业名称时是否抛出404错误。0为否1为是默认为否。可以避免传入不存在企业时扣减次数。\t"
type: integer
position: query
requestTemplate:
url: https://api.81api.com/getCompanyJobsInfo/{CompanyName}/
method: GET
headers:
- key: Authorization
value: APPCODE {{.config.appCode}}
- key: X-Ca-Nonce
value: '{{uuidv4}}'
responseTemplate:
prependBody: |+
# API Response Information
Below is the response from an API call. To help you understand the data, I've provided:
1. A detailed description of all fields in the response structure
2. The complete API response
## Response Structure
> Content-Type: application/json
- **data**: (Type: object)
- **data.list**: (Type: array)
- **data.list[].date**: 发布日期 (Type: string)
- **data.list[].education**: 学历要求 (Type: string)
- **data.list[].position**: 职位名称 (Type: string)
- **data.list[].salary**: 薪资范围 (Type: string)
- **data.list[].years**: 工作年限 (Type: string)
- **data.total**: 总数 (Type: integer)
- **status**: 响应状态 (Type: boolean)
## Original Response
- name: business-lawsuit-query
description: 企业法律诉讼信息,主要是裁判文书
args:
- name: CompanyName
description: 传入企业全称
type: string
required: true
position: path
- name: isRaiseErrorCode
description: "当请求传入不存在企业名称时是否抛出404错误。0为否1为是默认为否。可以避免传入不存在企业时扣减次数。\t"
type: integer
position: query
requestTemplate:
url: https://api.81api.com/getCompanyLawsuitInfo/{CompanyName}/
method: GET
headers:
- key: Authorization
value: APPCODE {{.config.appCode}}
- key: X-Ca-Nonce
value: '{{uuidv4}}'
responseTemplate:
prependBody: |+
# API Response Information
Below is the response from an API call. To help you understand the data, I've provided:
1. A detailed description of all fields in the response structure
2. The complete API response
## Response Structure
> Content-Type: application/json
- **data**: (Type: array)
- **data[].caseContent**: 案件内容 (Type: string)
- **data[].caseName**: 案件名称 (Type: string)
- **data[].caseNo**: 案号 (Type: string)
- **data[].caseReason**: 案由 (Type: string)
- **data[].pulishDate**: 发布日期 (Type: string)
- **status**: 请求状态 (Type: boolean)
## Original Response
- name: business-court-query
description: 企业法院公告信息
args:
- name: CompanyName
description: CompanyName
type: string
required: true
position: path
- name: isRaiseErrorCode
description: "当请求传入不存在企业名称时是否抛出404错误。0为否1为是默认为否。可以避免传入不存在企业时扣减次数。\t"
type: integer
position: query
requestTemplate:
url: https://api.81api.com/getCompanyCourtInfo/{CompanyName}/
method: GET
headers:
- key: Authorization
value: APPCODE {{.config.appCode}}
- key: X-Ca-Nonce
value: '{{uuidv4}}'
responseTemplate:
prependBody: |+
# API Response Information
Below is the response from an API call. To help you understand the data, I've provided:
1. A detailed description of all fields in the response structure
2. The complete API response
## Response Structure
> Content-Type: application/json
- **data**: (Type: array)
- **data[].courtName**: 法庭名称 (Type: string)
- **data[].courtNo**: 案件编号 (Type: string)
- **data[].pulishDate**: 发布日期和时间 (Type: string)
- **status**: 请求状态 (Type: boolean)
## Original Response
- name: business-abnormal-query
description: 企业经营异常信息
args:
- name: CompanyName
description: CompanyName
type: string
required: true
position: path
- name: isRaiseErrorCode
description: "当请求传入不存在企业名称时是否抛出404错误。0为否1为是默认为否。可以避免传入不存在企业时扣减次数。\t"
type: integer
position: query
requestTemplate:
url: https://api.81api.com/getCompanyAbnormalInfo/{CompanyName}/
method: GET
headers:
- key: Authorization
value: APPCODE {{.config.appCode}}
- key: X-Ca-Nonce
value: '{{uuidv4}}'
responseTemplate:
prependBody: |+
# API Response Information
Below is the response from an API call. To help you understand the data, I've provided:
1. A detailed description of all fields in the response structure
2. The complete API response
## Response Structure
> Content-Type: application/json
- **data**: (Type: object)
- **data.list**: (Type: array)
- **data.list[].iDate**: (Type: string)
- **data.list[].iReason**: (Type: string)
- **data.list[].oDate**: (Type: string)
- **data.list[].oReason**: (Type: string)
- **data.list[].orgName**: (Type: string)
- **data.total**: (Type: integer)
- **status**: (Type: boolean)
## Original Response
- name: business-financing-query
description: 企业融资信息
args:
- name: CompanyName
description: CompanyName
type: string
required: true
position: path
- name: isRaiseErrorCode
description: "当请求传入不存在企业名称时是否抛出404错误。0为否1为是默认为否。可以避免传入不存在企业时扣减次数。\t"
type: integer
position: query
requestTemplate:
url: https://api.81api.com/getCompanyFinancingInfo/{CompanyName}/
method: GET
headers:
- key: Authorization
value: APPCODE {{.config.appCode}}
- key: X-Ca-Nonce
value: '{{uuidv4}}'
responseTemplate:
prependBody: |+
# API Response Information
Below is the response from an API call. To help you understand the data, I've provided:
1. A detailed description of all fields in the response structure
2. The complete API response
## Response Structure
> Content-Type: application/json
- **data**: (Type: array)
- **data[].amount**: 投资金额 (Type: string)
- **data[].date**: 投资日期 (Type: string)
- **data[].investors**: 投资者列表 (Type: string)
- **data[].round**: 融资轮次 (Type: string)
- **status**: 状态标识true表示成功 (Type: boolean)
## Original Response
- name: business-debtor-query
description: 企业被执行人信息
args:
- name: CompanyName
description: 传入企业全称
type: string
required: true
position: path
- name: isRaiseErrorCode
description: "当请求传入不存在企业名称时是否抛出404错误。0为否1为是默认为否。可以避免传入不存在企业时扣减次数。\t"
type: integer
position: query
requestTemplate:
url: https://api.81api.com/getCompanyJudgmentDebtorInfo/{CompanyName}/
method: GET
headers:
- key: Authorization
value: APPCODE {{.config.appCode}}
- key: X-Ca-Nonce
value: '{{uuidv4}}'
responseTemplate:
prependBody: |+
# API Response Information
Below is the response from an API call. To help you understand the data, I've provided:
1. A detailed description of all fields in the response structure
2. The complete API response
## Response Structure
> Content-Type: application/json
- **data**: (Type: object)
- **data.list**: (Type: array)
- **data.list[].caseMoney**: 案件金额 (Type: string)
- **data.list[].caseNo**: 案件编号 (Type: string)
- **data.list[].caseOrg**: 案件所属法院 (Type: string)
- **data.list[].parties**: 当事人 (Type: string)
- **data.list[].pulishDate**: 发布日期 (Type: string)
- **data.total**: 数据总数 (Type: integer)
- **status**: 状态标识true表示成功 (Type: boolean)
## Original Response
- name: business-software-copyrights-query
description: 企业软件著作权信息
args:
- name: CompanyName
description: CompanyName
type: string
required: true
position: path
- name: isRaiseErrorCode
description: "当请求传入不存在企业名称时是否抛出404错误。0为否1为是默认为否。可以避免传入不存在企业时扣减次数。\t"
type: integer
position: query
requestTemplate:
url: https://api.81api.com/getCompanySoftwareCopyrightsInfo/{CompanyName}/
method: GET
headers:
- key: Authorization
value: APPCODE {{.config.appCode}}
- key: X-Ca-Nonce
value: '{{uuidv4}}'
responseTemplate:
prependBody: |+
# API Response Information
Below is the response from an API call. To help you understand the data, I've provided:
1. A detailed description of all fields in the response structure
2. The complete API response
## Response Structure
> Content-Type: application/json
- **data**: (Type: object)
- **data.list**: (Type: array)
- **data.list[].name**: 软件名称 (Type: string)
- **data.list[].publishDate**: 发布日期 (Type: string)
- **data.list[].regNo**: 注册号 (Type: string)
- **data.list[].shortName**: 简称 (Type: string)
- **data.list[].typeNo**: 类型编号 (Type: string)
- **data.list[].versionNo**: 版本号 (Type: string)
- **data.total**: 总数量 (Type: integer)
- **status**: 请求状态true表示成功 (Type: boolean)
## Original Response
- name: business-profile-tags-query
description: 基于大数据对企业的画像标签信息
args:
- name: CompanyName
description: CompanyName
type: string
required: true
position: path
- name: isRaiseErrorCode
description: 当请求传入不存在企业名称时是否抛出404错误。0为否1为是默认为否。可以避免传入不存在企业时扣减次数。
type: integer
position: query
requestTemplate:
url: https://api.81api.com/getCompanyProfileTags/{CompanyName}/
method: GET
headers:
- key: Authorization
value: APPCODE {{.config.appCode}}
- key: X-Ca-Nonce
value: '{{uuidv4}}'
responseTemplate:
prependBody: |+
# API Response Information
Below is the response from an API call. To help you understand the data, I've provided:
1. A detailed description of all fields in the response structure
2. The complete API response
## Response Structure
> Content-Type: application/json
- **data**: 数据列表 (Type: array)
- **data[]**: Items of type string
- **status**: 响应状态true表示成功 (Type: boolean)
## Original Response

View File

@@ -0,0 +1,30 @@
# Enterprise Patent Query
The APP Code required for API authentication can be applied for at the Alibaba Cloud API Marketplace: https://market.aliyun.com/apimarket/detail/cmapi00049059
# MCP Server Configuration Document
This server is primarily used for querying enterprise patent information, supporting the retrieval of patent lists and detailed information.
## Function Overview
The `business-patent-query` server focuses on providing services related to patent information for enterprises or individual users. Through this service, users can easily search for all relevant patents within a specific technical field, which helps in avoiding infringement of others' intellectual property rights and guiding their own R&D activities. It includes two core functions: patent information list retrieval and patent detail viewing.
## Tool Introduction
### 1. Patent Information List
- **Purpose**: This tool allows users to find related patent lists based on keywords (such as company name, social credit code, etc.).
- **Use Cases**: It is used when a comprehensive understanding of the patent layout of a particular industry or company is needed; it can also be used for market research, competitor analysis, and other areas.
- **Parameter Description**:
- `dtype`: The format of the returned data, default is JSON.
- `keyword`: A required parameter, used to specify the search keyword.
- `pageIndex`: Specifies the page number of the returned results, default is the first page.
- `pageSize`: Sets the number of results displayed per page, default is 10 records, with a maximum of 10 records.
### 2. Patent Information Details
- **Purpose**: Based on a known patent ID, this tool can obtain the specific details of a single patent.
- **Use Cases**: It is suitable for in-depth study of a specific patent content or when detailed information about a certain technical solution is needed.
- **Parameter Description**:
- `dtype`: Defines the format of the response data, default is JSON.
- `id`: A required field, representing the unique identifier of the patent to be queried, typically obtained from the "Patent Information List" interface.
Each tool provides detailed request and response templates to ensure that developers can correctly call the API and handle the returned data. Additionally, the response structure for each tool includes basic information about the requested patent as well as some additional metadata, such as order number, status code, etc., to facilitate tracking the request status and parsing the data.

View File

@@ -0,0 +1,41 @@
# 企业专利查询
API认证需要的APP Code请在阿里云API市场申请: https://market.aliyun.com/apimarket/detail/cmapi00049059
## 什么是云市场API MCP服务
阿里云云市场是生态伙伴的交易服务平台我们致力于为合作伙伴提供覆盖上云、商业化和售卖的全链路服务帮助客户高效获取、部署和管理优质生态产品。云市场的API服务涵盖以下几个类目应用开发、身份验证与金融、车辆交通与物流、企业服务、短信与运营商、AI应用与OCR、生活服务。
云市场API依托Higress提供MCP服务您只需在云市场完成订阅并获取AppCode通过Higress MCP Server进行配置即可无缝集成云市场API服务。
## 如何在使用云市场API MCP服务
1. 进入API详情页订阅该API。您可以优先使用免费试用。
2. 前往云市场用户控制台使用阿里云账号登陆后查看已订阅API服务的AppCode并配置到Higress MCP Server的配置中。注意在阿里云市场订阅API服务后您将获得AppCode。对于您订阅的所有API服务此AppCode是相同的您只需使用这一个AppCode即可访问所有已订阅的API服务。
3. 云市场用户控制台会实时展示已订阅的预付费API服务的可用额度如您免费试用额度已用完您可以选择重新订阅。
# MCP服务器配置文档
该服务器主要用于查询企业的专利信息,支持获取专利列表及详细信息。
## 功能简介
`business-patent-query`服务器专注于为企业或个人用户提供专利相关信息的服务。通过此服务,用户可以轻松地搜索到特定技术领域内的所有相关专利,这有助于避免侵犯他人的知识产权,并为自身的研发活动指明方向。它包括两大核心功能:专利信息列表检索与专利详情查看。
## 工具简介
### 1. 专利信息列表
- **用途**:此工具允许用户根据关键字(如公司名称、社会统一信用代码等)来查找相关的专利列表。
- **应用场景**:当需要对某一行业或公司的专利布局进行全面了解时使用;也可用于市场调研、竞争对手分析等领域。
- **参数说明**
- `dtype`: 返回的数据格式默认为JSON。
- `keyword`: 必填项,用来指定搜索的关键字。
- `pageIndex`: 指定返回结果的页码,默认第一页。
- `pageSize`: 设置每页显示的结果数量默认为10条记录最大不超过10条。
### 2. 专利信息详情
- **用途**基于已知的专利ID此工具能够获取单个专利的具体细节信息。
- **应用场景**:适用于深入研究某一项具体的专利内容,或是需要详细了解某项技术解决方案的情况。
- **参数说明**
- `dtype`: 同样定义了响应数据的格式默认采用JSON形式。
- `id`: 必需字段代表想要查询的专利唯一标识符通常是从“专利信息列表”接口获得的ID值。
每个工具都提供了详细的请求模板和响应模板说明以确保开发者能够正确调用API并处理返回的数据。此外对于每个工具而言其响应结构均包含关于所请求专利的基本信息以及额外的一些元数据比如订单号、状态码等便于跟踪请求状态和解析数据。

View File

@@ -0,0 +1,380 @@
{
"info": {
"description": "查询企业或某技术的专利,帮助用户掌握相同技术领域的发展状况,为规避他人知识产权和调整研发方向提供参考",
"title": "企业专利信息-专利信息查询",
"version": "1.0.0"
},
"openapi": "3.0.1",
"paths": {
"/utn/ip/PatentDetail": {
"get": {
"operationId": "专利信息详情",
"summary": "查询企业或某技术的专利,帮助用户掌握相同技术领域的发展状况,为规避他人知识产权和调整研发方向提供参考",
"parameters": [
{
"description": "专利信息列表接口返回的Id",
"in": "query",
"name": "id",
"required": true,
"schema": {
"type": "integer"
}
},
{
"description": "返回数据格式json或xml默认json",
"in": "query",
"name": "dtype",
"required": false,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"properties": {
"orderNo": {
"description": "订单编号",
"example": "1359050786293813200",
"type": "integer"
},
"data": {
"properties": {
"LegalStatusDate": {
"description": "法律状态日期",
"example": "2019-11-15 00:00:00",
"format": "date-time",
"type": "string"
},
"Agent": {
"description": "代理人",
"example": "肖平安",
"type": "string"
},
"PublicationDate": {
"description": "公布日期",
"example": "2018-11-13 00:00:00",
"format": "date-time",
"type": "string"
},
"Agency": {
"description": "代理机构",
"example": "北京科亿知识产权代理事务所(普通合伙)",
"type": "string"
},
"OtherReferences": {
"description": "其他引用",
"nullable": true,
"type": "string"
},
"IPCList": {
"description": "国际专利分类号列表",
"example": "H04L9/32",
"type": "string"
},
"LegalStatusDesc": {
"description": "法律状态描述",
"example": "授权",
"type": "string"
},
"Abstract": {
"description": "摘要",
"example": "本发明公开了一种基于非对称密码算法的保护隐私征信方法...",
"type": "string"
},
"Title": {
"description": "标题",
"example": "一种基于非对称密码算法的保护隐私征信方法",
"type": "string"
},
"KindCodeDesc": {
"description": "类型代码描述",
"example": "发明",
"type": "string"
},
"PrimaryExaminer": {
"description": "主审查员",
"nullable": true,
"type": "string"
},
"AssiantExaminer": {
"description": "辅助审查员",
"nullable": true,
"type": "string"
},
"ApplicationDate": {
"description": "申请日期",
"example": "2015-05-13 00:00:00",
"format": "date-time",
"type": "string"
},
"PatentImage": {
"description": "专利图片链接",
"example": "https://filecdn.shuidi.cn/img/upload/images_patent/cc/b4/eb/ccb4ebc86ad8b0093fcc0c30999be8fc.png/0x0.jpg",
"type": "string"
},
"AssigneestringList": {
"description": "专利权人列表",
"example": "上海凭安企业信用征信有限公司,上海凭安征信服务有限公司",
"type": "string"
},
"PatentLegalHistory": {
"items": {
"properties": {
"LegalStatusDate": {
"description": "法律状态日期",
"example": "2019-11-15 00:00:00",
"format": "date-time",
"type": "string"
},
"Desc": {
"description": "描述",
"example": "专利权人的姓名或者名称、地址的变更IPC(主分类):H04L 9/32变更前 专利权人:上海凭安企业信用征信有限公司 地址:201700 上海市长宁区广顺路33号8幢193室变更后 专利权人:上海凭安征信服务有限公司 地址:200335 上海市长宁区广顺路33号8幢193室",
"type": "string"
},
"LegalStatus": {
"description": "法律状态",
"example": "专利权人的姓名或者名称、地址的变更",
"type": "string"
}
},
"type": "object"
},
"type": "array"
},
"InventorStringList": {
"description": "发明人列表",
"example": "韩洪慧,杨茂江",
"type": "string"
},
"IPCDesc": {
"description": "国际专利分类号描述",
"example": "包括用于检验系统用户的身份或凭据的装置5",
"type": "string"
},
"DocumentTypes": {
"description": "文档类型",
"nullable": true,
"type": "string"
},
"ApplicationNumber": {
"description": "申请号",
"example": "CN201510241189.8",
"type": "string"
},
"PublicationNumber": {
"description": "公布号",
"example": "CN104821883B",
"type": "string"
},
"Cites": {
"description": "引用",
"nullable": true,
"type": "string"
}
},
"type": "object"
},
"statusMessage": {
"description": "状态消息",
"example": "请求成功",
"type": "string"
},
"statusCode": {
"description": "状态码",
"example": "1",
"type": "integer"
}
},
"type": "object"
}
}
},
"description": "请求成功"
}
}
}
},
"/utn/ip/PatentPageByKey/V2": {
"get": {
"operationId": "专利信息列表",
"summary": "查询企业或某技术的专利,帮助用户掌握相同技术领域的发展状况,为规避他人知识产权和调整研发方向提供参考",
"parameters": [
{
"description": "搜索关键字(公司名称、社会统一信用代码、注册号)",
"in": "query",
"name": "keyword",
"required": true,
"schema": {
"type": "string"
}
},
{
"description": "页码默认第1页",
"in": "query",
"name": "pageIndex",
"required": false,
"schema": {
"type": "integer"
}
},
{
"description": "每页条数默认为10最大不超过10条",
"in": "query",
"name": "pageSize",
"required": false,
"schema": {
"type": "integer"
}
},
{
"description": "返回数据格式json或xml默认json",
"in": "query",
"name": "dtype",
"required": false,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"properties": {
"orderNo": {
"description": "订单号",
"example": "1359050786163789800",
"type": "integer"
},
"data": {
"properties": {
"Paging": {
"properties": {
"PageSize": {
"description": "每页显示条数",
"example": "10",
"type": "integer"
},
"TotalRecords": {
"description": "总记录数",
"example": "8842",
"type": "integer"
},
"PageIndex": {
"description": "当前页码",
"example": "3",
"type": "integer"
}
},
"type": "object"
},
"Items": {
"items": {
"properties": {
"PublicationDate": {
"description": "公开日期",
"example": "2018-04-27 00:00:00",
"format": "date-time",
"type": "string"
},
"Agency": {
"description": "代理机构",
"example": "北京三高永信知识产权代理有限责任公司",
"type": "string"
},
"IPCList": {
"description": "IPC分类号",
"example": "H04M7/00",
"type": "string"
},
"LegalStatusDesc": {
"description": "法律状态描述",
"example": "授权",
"type": "string"
},
"Title": {
"description": "标题",
"example": "语音通道建立方法、装置及系统",
"type": "string"
},
"KindCodeDesc": {
"description": "类别代码描述",
"example": "发明",
"type": "string"
},
"ApplicationDate": {
"description": "申请日期",
"example": "2015-06-26 00:00:00",
"format": "date-time",
"type": "string"
},
"AssigneeStringList": {
"description": "申请人",
"example": "小米科技有限责任公司",
"type": "string"
},
"InventorStringList": {
"description": "发明人",
"example": "侯俊杰,辛显龙,金峰",
"type": "string"
},
"IPCDesc": {
"description": "IPC分类描述",
"example": "交换中心之间的互连装置",
"type": "string"
},
"ApplicationNumber": {
"description": "申请号",
"example": "CN201510363777.9",
"type": "string"
},
"Id": {
"description": "ID",
"example": "45233394",
"type": "integer"
},
"PublicationNumber": {
"description": "公开号",
"example": "CN105100523B",
"type": "string"
}
},
"type": "object"
},
"type": "array"
}
},
"type": "object"
},
"statusMessage": {
"description": "状态消息",
"example": "请求成功",
"type": "string"
},
"statusCode": {
"description": "状态码",
"example": "1",
"type": "integer"
}
},
"type": "object"
}
}
},
"description": "请求成功"
}
}
}
}
},
"servers": [
{
"url": "http://icpatent.market.alicloudapi.com"
}
]
}

View File

@@ -0,0 +1,136 @@
server:
name: business-patent-query
config:
appCode: ""
tools:
- name: business-patent-query
description: 查询企业或某技术的专利,帮助用户掌握相同技术领域的发展状况,为规避他人知识产权和调整研发方向提供参考
args:
- name: dtype
description: 返回数据格式json或xml默认json
type: string
position: query
- name: keyword
description: 搜索关键字(公司名称、社会统一信用代码、注册号)
type: string
required: true
position: query
- name: pageIndex
description: 页码默认第1页
type: integer
position: query
- name: pageSize
description: 每页条数默认为10最大不超过10条
type: integer
position: query
requestTemplate:
url: http://icpatent.market.alicloudapi.com/utn/ip/PatentPageByKey/V2
method: GET
headers:
- key: Authorization
value: APPCODE {{.config.appCode}}
- key: X-Ca-Nonce
value: '{{uuidv4}}'
responseTemplate:
prependBody: |+
# API Response Information
Below is the response from an API call. To help you understand the data, I've provided:
1. A detailed description of all fields in the response structure
2. The complete API response
## Response Structure
> Content-Type: application/json
- **data**: (Type: object)
- **data.Items**: (Type: array)
- **data.Items[].Agency**: 代理机构 (Type: string)
- **data.Items[].ApplicationDate**: 申请日期 (Type: string)
- **data.Items[].ApplicationNumber**: 申请号 (Type: string)
- **data.Items[].AssigneeStringList**: 申请人 (Type: string)
- **data.Items[].IPCDesc**: IPC分类描述 (Type: string)
- **data.Items[].IPCList**: IPC分类号 (Type: string)
- **data.Items[].Id**: ID (Type: integer)
- **data.Items[].InventorStringList**: 发明人 (Type: string)
- **data.Items[].KindCodeDesc**: 类别代码描述 (Type: string)
- **data.Items[].LegalStatusDesc**: 法律状态描述 (Type: string)
- **data.Items[].PublicationDate**: 公开日期 (Type: string)
- **data.Items[].PublicationNumber**: 公开号 (Type: string)
- **data.Items[].Title**: 标题 (Type: string)
- **data.Paging**: (Type: object)
- **data.Paging.PageIndex**: 当前页码 (Type: integer)
- **data.Paging.PageSize**: 每页显示条数 (Type: integer)
- **data.Paging.TotalRecords**: 总记录数 (Type: integer)
- **orderNo**: 订单号 (Type: integer)
- **statusCode**: 状态码 (Type: integer)
- **statusMessage**: 状态消息 (Type: string)
## Original Response
- name: patent-detail
description: 查询企业或某技术的专利,帮助用户掌握相同技术领域的发展状况,为规避他人知识产权和调整研发方向提供参考
args:
- name: dtype
description: 返回数据格式json或xml默认json
type: string
position: query
- name: id
description: 专利信息列表接口返回的Id
type: integer
required: true
position: query
requestTemplate:
url: http://icpatent.market.alicloudapi.com/utn/ip/PatentDetail
method: GET
headers:
- key: Authorization
value: APPCODE {{.config.appCode}}
- key: X-Ca-Nonce
value: '{{uuidv4}}'
responseTemplate:
prependBody: |+
# API Response Information
Below is the response from an API call. To help you understand the data, I've provided:
1. A detailed description of all fields in the response structure
2. The complete API response
## Response Structure
> Content-Type: application/json
- **data**: (Type: object)
- **data.Abstract**: 摘要 (Type: string)
- **data.Agency**: 代理机构 (Type: string)
- **data.Agent**: 代理人 (Type: string)
- **data.ApplicationDate**: 申请日期 (Type: string)
- **data.ApplicationNumber**: 申请号 (Type: string)
- **data.AssiantExaminer**: 辅助审查员 (Type: string)
- **data.AssigneestringList**: 专利权人列表 (Type: string)
- **data.Cites**: 引用 (Type: string)
- **data.DocumentTypes**: 文档类型 (Type: string)
- **data.IPCDesc**: 国际专利分类号描述 (Type: string)
- **data.IPCList**: 国际专利分类号列表 (Type: string)
- **data.InventorStringList**: 发明人列表 (Type: string)
- **data.KindCodeDesc**: 类型代码描述 (Type: string)
- **data.LegalStatusDate**: 法律状态日期 (Type: string)
- **data.LegalStatusDesc**: 法律状态描述 (Type: string)
- **data.OtherReferences**: 其他引用 (Type: string)
- **data.PatentImage**: 专利图片链接 (Type: string)
- **data.PatentLegalHistory**: (Type: array)
- **data.PatentLegalHistory[].Desc**: 描述 (Type: string)
- **data.PatentLegalHistory[].LegalStatus**: 法律状态 (Type: string)
- **data.PatentLegalHistory[].LegalStatusDate**: 法律状态日期 (Type: string)
- **data.PrimaryExaminer**: 主审查员 (Type: string)
- **data.PublicationDate**: 公布日期 (Type: string)
- **data.PublicationNumber**: 公布号 (Type: string)
- **data.Title**: 标题 (Type: string)
- **orderNo**: 订单编号 (Type: integer)
- **statusCode**: 状态码 (Type: integer)
- **statusMessage**: 状态消息 (Type: string)
## Original Response

View File

@@ -0,0 +1,43 @@
# Chinese Almanac/Holiday Helper
The APP Code required for API authentication can be applied for at the Alibaba Cloud API Marketplace: https://market.aliyun.com/apimarket/detail/cmapi00066017
# MCP Server Configuration Document
## Function Overview
The `calendar-holiday-helper` server is a service platform focused on providing holiday-related information and almanac fortune queries. It supports various API calls to obtain data including but not limited to holiday lists, detailed holiday information for specific dates, and almanac information based on traditional Chinese culture. These services are very useful for individuals and organizations that need to schedule activities based on specific dates or want to know the auspiciousness of a particular day.
## Tool Introduction
### 1. Holiday List
- **Description**: This tool is used to list all holidays within a specified year.
- **Use Case**: Suitable for businesses planning annual holidays, the travel industry formulating promotional plans, etc.
- **Parameter Description**:
- `year` (string): The year to query, defaulting to the current year. For non-current years, it also returns the current year's holiday data; next year's data can only be queried in December of the current year.
### 2. Holiday Details
- **Description**: This tool provides detailed holiday information for a specific date (defaulting to the current day).
- **Use Case**: Suitable for individuals or teams who want to know if a particular day is a holiday and its specific name.
- **Parameter Description**:
- `date` (string): The date to query, defaulting to the current day.
- `needDesc` (string): Whether to return a brief description of public holidays, international days, and traditional Chinese festivals, with a value of 1 indicating to return, defaulting to not returning.
### 3. Almanac Fortune (New Version) - Auspicious Times
- **Description**: Provides a daily auspicious time query service based on the traditional Chinese calendar.
- **Use Case**: Particularly useful for those who believe in choosing auspicious times for important decisions.
- **Parameter Description**:
- `date` (string, required): The date to query, in the format yyyyMMdd.
### 4. Almanac Fortune (New Version) - Auspicious Deities and Inauspicious Spirits
- **Description**: Displays information about auspicious deities and inauspicious spirits affecting fortune on a specific date.
- **Use Case**: Helps users avoid unfavorable factors and seize favorable opportunities.
- **Parameter Description**:
- `date` (string, required): The date to query, in the format yyyyMMdd.
### 5. Almanac Fortune (New Version) - Almanac
- **Description**: A comprehensive calendar service integrating the lunar calendar, Gregorian calendar, and other relevant astronomical information.
- **Use Case**: Widely used for arranging various customary activities in daily life.
- **Parameter Description**:
- `date` (string, required): The date to query, in the format yyyyMMdd.
The above is an overview of the main tools and services provided by the `calendar-holiday-helper` server. By making reasonable use of these tools, users can more effectively manage their time and adjust their activity schedules as needed.

View File

@@ -0,0 +1,54 @@
# 中国黄历/假期助手
API认证需要的APP Code请在阿里云API市场申请: https://market.aliyun.com/apimarket/detail/cmapi00066017
## 什么是云市场API MCP服务
阿里云云市场是生态伙伴的交易服务平台我们致力于为合作伙伴提供覆盖上云、商业化和售卖的全链路服务帮助客户高效获取、部署和管理优质生态产品。云市场的API服务涵盖以下几个类目应用开发、身份验证与金融、车辆交通与物流、企业服务、短信与运营商、AI应用与OCR、生活服务。
云市场API依托Higress提供MCP服务您只需在云市场完成订阅并获取AppCode通过Higress MCP Server进行配置即可无缝集成云市场API服务。
## 如何在使用云市场API MCP服务
1. 进入API详情页订阅该API。您可以优先使用免费试用。
2. 前往云市场用户控制台使用阿里云账号登陆后查看已订阅API服务的AppCode并配置到Higress MCP Server的配置中。注意在阿里云市场订阅API服务后您将获得AppCode。对于您订阅的所有API服务此AppCode是相同的您只需使用这一个AppCode即可访问所有已订阅的API服务。
3. 云市场用户控制台会实时展示已订阅的预付费API服务的可用额度如您免费试用额度已用完您可以选择重新订阅。
# MCP服务器配置文档
## 功能简介
`calendar-holiday-helper`服务器是一个专注于提供节假日相关信息以及黄历运势查询的服务平台。它支持多种API调用来获取包括但不限于节假日列表、具体日期的节假日详情、以及基于中国传统文化的黄历信息等数据。这些服务对于需要根据特定日期安排活动或希望了解某日吉凶情况的个人和组织非常有用。
## 工具简介
### 1. 节假日列表
- **描述**:此工具用于列出指定年份内的所有节假日。
- **应用场景**:适用于企业规划年度假期、旅游行业制定促销计划等场合。
- **参数说明**
- `year` (string)需要查询的年份默认查当年。非当年日期也返回当年节假日数据来年的数据需等到当年12月份才能查询。
### 2. 节假日详情
- **描述**:该工具提供了一个具体的日期(默认为当天)下的节假日详细信息。
- **应用场景**:适合于个人或团队想要了解某一天是否为节假日及其具体名称时使用。
- **参数说明**
- `date` (string):查询的日期,默认为当天。
- `needDesc` (string)是否需要返回当日公众日、国际日和我国传统节日的简介值为1表示返回默认不返回。
### 3. 黄历运势_新版_吉时
- **描述**:提供了基于中国传统历法的每日吉时查询服务。
- **应用场景**:对那些相信选择吉时进行重要决策的人群特别有用。
- **参数说明**
- `date` (string, 必填)查询的日期格式为yyyyMMdd。
### 4. 黄历运势_新版_吉神凶煞
- **描述**:展示了特定日期内影响运势的吉神与凶煞信息。
- **应用场景**:帮助用户避开不利因素并抓住有利时机。
- **参数说明**
- `date` (string, 必填)查询的日期格式为yyyyMMdd。
### 5. 黄历运势_新版_黄历
- **描述**:综合了农历、公历以及其他相关天文学信息的日历服务。
- **应用场景**:广泛应用于日常生活中的各种习俗活动安排。
- **参数说明**
- `date` (string, 必填)查询的日期格式为yyyyMMdd。
以上就是`calendar-holiday-helper`服务器提供的主要工具和服务概述。通过合理利用这些工具,用户能够更有效地管理时间,并根据需要调整自己的活动安排。

View File

@@ -0,0 +1,909 @@
{
"info": {
"description": "【节假日查询 黄历查询 吉日查询 】接口可查询传统日历、节假日、运势、宜忌等信息,广泛用于日程安排,出行指南,风水评估等。 —— 我们只做精品!",
"title": "【聚美智数】黄历查询-日历查询-节假日查询-运势查询-吉凶查询-万年历-阴阳历-国际法定节假日查询",
"version": "1.0.0"
},
"openapi": "3.0.1",
"paths": {
"/holiday/list": {
"post": {
"operationId": "节假日列表",
"summary": "节假日列表",
"requestBody": {
"content": {
"application/x-www-form-urlencoded": {
"schema": {
"type": "object",
"properties": {
"year": {
"description": "需要查询的年份【注意: 默认查当年非当年日期也返回当年节假日数据来年数据需等到当年12月份才能查】",
"type": "string"
}
}
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"description": "返回码"
},
"msg": {
"type": "string",
"description": "返回信息"
},
"taskNo": {
"type": "string",
"description": "请求号"
},
"data": {
"type": "object",
"properties": {
"count": {
"type": "integer",
"description": "一年的节假日数量"
},
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"begin": {
"type": "string"
},
"end": {
"type": "string"
},
"holiday": {
"type": "string"
},
"holiday_remark": {
"type": "string"
},
"inverse_days": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
}
}
}
}
}
},
"description": "成功响应"
}
}
}
},
"/luck-tendency/almanac": {
"post": {
"operationId": "黄历运势_新版_黄历",
"summary": "黄历运势_新版_黄历",
"requestBody": {
"content": {
"application/x-www-form-urlencoded": {
"schema": {
"type": "object",
"properties": {
"date": {
"description": "查询的日期 格式为yyyyMMdd",
"type": "string"
}
},
"required": [
"date"
]
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"msg": {
"type": "string",
"example": "成功"
},
"code": {
"type": "integer",
"example": 200
},
"taskNo": {
"type": "string",
"example": 74848319667949360000
},
"data": {
"type": "object",
"properties": {
"gongli": {
"type": "string",
"description": "公历"
},
"nongli": {
"type": "string",
"description": "农历"
},
"jieri": {
"type": "string",
"description": "节日"
},
"zhiri": {
"type": "string",
"description": "值日"
},
"zhishen": {
"type": "string",
"description": "值神"
},
"yi": {
"type": "string",
"description": "宜"
},
"ji": {
"type": "string",
"description": "忌"
},
"qixiang": {
"type": "string",
"description": "气象"
},
"jieqi24": {
"type": "string",
"description": "当前月包含的24节气"
},
"shengxiao": {
"type": "string",
"description": "生肖"
},
"xingzuo": {
"type": "string",
"description": "星座"
},
"rulueli": {
"type": "string",
"description": "儒略历"
},
"jsyq": {
"type": "string",
"description": "吉神宜趋"
},
"xsyj": {
"type": "string",
"description": "凶神宜忌"
},
"pzbj": {
"type": "string",
"description": "彭祖百忌"
},
"tszf": {
"type": "string",
"description": "胎神占方"
},
"chongsha": {
"type": "string",
"description": "冲煞"
},
"nayin": {
"type": "string",
"description": "纳音"
},
"dizhi": {
"type": "string",
"description": "地支"
},
"ganzhi": {
"type": "string",
"description": "干支"
}
}
}
}
}
}
},
"description": "成功响应"
}
}
}
},
"/luck-tendency/auspicious-demon": {
"post": {
"operationId": "黄历运势_新版_吉神凶煞",
"summary": "黄历运势_新版_吉神凶煞",
"requestBody": {
"content": {
"application/x-www-form-urlencoded": {
"schema": {
"type": "object",
"properties": {
"date": {
"description": "查询的日期 格式为yyyyMMdd",
"type": "string"
}
},
"required": [
"date"
]
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"msg": {
"type": "string",
"example": "成功"
},
"code": {
"type": "integer",
"example": 200
},
"taskNo": {
"type": "string",
"example": "74848319667949359984"
},
"data": {
"type": "object",
"properties": {
"niansansha": {
"type": "string"
},
"nianqisha": {
"type": "string"
},
"niankongwang": {
"type": "string"
},
"yuezhi": {
"type": "string"
},
"yueling": {
"type": "string"
},
"yuexiang": {
"type": "string"
},
"yuesansha": {
"type": "string"
},
"yueqisha": {
"type": "string"
},
"yuekongwang": {
"type": "string"
},
"risansha": {
"type": "string"
},
"riqisha": {
"type": "string"
},
"rikongwang": {
"type": "string"
},
"tjjs": {
"type": "string"
},
"taisuiwei": {
"type": "string"
},
"fantaisui": {
"type": "string"
},
"esbx": {
"type": "string"
},
"jiuxing": {
"type": "string"
},
"rilu": {
"type": "string"
},
"zhongdong": {
"type": "string"
},
"suipowei": {
"type": "string"
},
"niantaisui": {
"type": "string"
},
"caishen": {
"type": "string"
},
"xishen": {
"type": "string"
},
"yangguishen": {
"type": "string"
},
"yinguishen": {
"type": "string"
},
"fushen": {
"type": "string"
},
"yjgx": {
"type": "string"
},
"wuhou": {
"type": "string"
},
"zhishn12": {
"type": "string"
},
"zhiri12": {
"type": "string"
},
"liuyao": {
"type": "string"
}
}
}
}
}
}
},
"description": "成功响应"
}
}
}
},
"/luck-tendency/auspicious-time": {
"post": {
"operationId": "黄历运势_新版_吉时",
"summary": "黄历运势_新版_吉时",
"requestBody": {
"content": {
"application/x-www-form-urlencoded": {
"schema": {
"type": "object",
"properties": {
"date": {
"description": "查询的日期 格式为yyyyMMdd",
"type": "string"
}
},
"required": [
"date"
]
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"msg": {
"type": "string",
"example": "成功"
},
"code": {
"type": "integer",
"example": 200
},
"taskNo": {
"type": "string",
"example": 74848319667949360000
},
"data": {
"type": "object",
"properties": {
"zi": {
"type": "object",
"properties": {
"shijian": {
"type": "string",
"example": "23:00:00-0:59:59"
},
"jixiong": {
"type": "string",
"example": "司命(吉)"
},
"jishen": {
"type": "string",
"example": "司命天乙贵人"
},
"xiongshen": {
"type": "string",
"example": "日刑"
},
"shichong": {
"type": "string",
"example": "冲马"
},
"shizhu": {
"type": "string",
"example": "甲子"
}
}
},
"hai": {
"type": "object",
"properties": {
"jixiong": {
"type": "string",
"example": "玄武(凶)"
},
"jishen": {
"type": "string",
"example": "无"
},
"shijian": {
"type": "string",
"example": "21:00:00-22:59:59"
},
"xiongshen": {
"type": "string",
"example": "玄武"
},
"shichong": {
"type": "string",
"example": "冲蛇"
},
"shizhu": {
"type": "string",
"example": "乙亥"
}
}
},
"wei": {
"type": "object",
"properties": {
"jixiong": {
"type": "string",
"example": "天德(吉)"
},
"jishen": {
"type": "string",
"example": "天德福星贵人"
},
"shijian": {
"type": "string",
"example": "13:00:00-14:59:59"
},
"xiongshen": {
"type": "string",
"example": "无"
},
"shichong": {
"type": "string",
"example": "冲牛"
},
"shizhu": {
"type": "string",
"example": "辛未"
}
}
},
"cheng": {
"type": "object",
"properties": {
"jixiong": {
"type": "string",
"example": "天刑(凶)"
},
"jishen": {
"type": "string",
"example": "无"
},
"shijian": {
"type": "string",
"example": "7:00:00-8:59:59"
},
"xiongshen": {
"type": "string",
"example": "天刑日害"
},
"shichong": {
"type": "string",
"example": "冲狗"
},
"shizhu": {
"type": "string",
"example": "戊辰"
}
}
},
"you": {
"type": "object",
"properties": {
"jixiong": {
"type": "string",
"example": "玉堂(吉)"
},
"jishen": {
"type": "string",
"example": "玉堂文昌贵人"
},
"shijian": {
"type": "string",
"example": "17:00:00-18:59:59"
},
"xiongshen": {
"type": "string",
"example": "日破"
},
"shichong": {
"type": "string",
"example": "冲兔"
},
"shizhu": {
"type": "string",
"example": "癸酉"
}
}
},
"wu": {
"type": "object",
"properties": {
"jixiong": {
"type": "string",
"example": "金匮(吉)"
},
"jishen": {
"type": "string",
"example": "金匮日禄"
},
"shijian": {
"type": "string",
"example": "11:00:00-12:59:59"
},
"xiongshen": {
"type": "string",
"example": "无"
},
"shichong": {
"type": "string",
"example": "冲鼠"
},
"shizhu": {
"type": "string",
"example": "庚午"
}
}
},
"si": {
"type": "object",
"properties": {
"jixiong": {
"type": "string",
"example": "朱雀(凶)"
},
"jishen": {
"type": "string",
"example": "日马"
},
"shijian": {
"type": "string",
"example": "9:00:00-10:59:59"
},
"xiongshen": {
"type": "string",
"example": "朱雀"
},
"shichong": {
"type": "string",
"example": "冲猪"
},
"shizhu": {
"type": "string",
"example": "己巳"
}
}
},
"xu": {
"type": "object",
"properties": {
"jixiong": {
"type": "string",
"example": "天牢(凶)"
},
"jishen": {
"type": "string",
"example": "日合"
},
"shijian": {
"type": "string",
"example": "19:00:00-20:59:59"
},
"xiongshen": {
"type": "string",
"example": "天牢"
},
"shichong": {
"type": "string",
"example": "冲龙"
},
"shizhu": {
"type": "string",
"example": "甲戌"
}
}
},
"chou": {
"type": "object",
"properties": {
"jixiong": {
"type": "string",
"example": "勾陈(凶)"
},
"jishen": {
"type": "string",
"example": "无"
},
"shijian": {
"type": "string",
"example": "1:00:00-2:59:59"
},
"xiongshen": {
"type": "string",
"example": "勾陈"
},
"shichong": {
"type": "string",
"example": "冲羊"
},
"shizhu": {
"type": "string",
"example": "乙丑"
}
}
},
"yin": {
"type": "object",
"properties": {
"jixiong": {
"type": "string",
"example": "青龙(吉)"
},
"jishen": {
"type": "string",
"example": "青龙喜神天官贵人"
},
"shijian": {
"type": "string",
"example": "3:00:00-4:59:59"
},
"xiongshen": {
"type": "string",
"example": "无"
},
"shichong": {
"type": "string",
"example": "冲猴"
},
"shizhu": {
"type": "string",
"example": "丙寅"
}
}
},
"shen": {
"type": "object",
"properties": {
"jixiong": {
"type": "string",
"example": "白虎(凶)"
},
"jishen": {
"type": "string",
"example": "天乙贵人"
},
"shijian": {
"type": "string",
"example": "15:00:00-16:59:59"
},
"xiongshen": {
"type": "string",
"example": "白虎"
},
"shichong": {
"type": "string",
"example": "冲虎"
},
"shizhu": {
"type": "string",
"example": "壬申"
}
}
},
"mao": {
"type": "object",
"properties": {
"jixiong": {
"type": "string",
"example": "明堂(吉)"
},
"jishen": {
"type": "string",
"example": "明堂"
},
"shijian": {
"type": "string",
"example": "5:00:00-6:59:59"
},
"xiongshen": {
"type": "string",
"example": "无"
},
"shichong": {
"type": "string",
"example": "冲鸡"
},
"shizhu": {
"type": "string",
"example": "丁卯"
}
}
}
}
}
}
}
}
},
"description": "成功响应"
}
}
}
},
"/holiday/detail": {
"post": {
"operationId": "节假日详情",
"summary": "节假日详情",
"requestBody": {
"content": {
"application/x-www-form-urlencoded": {
"schema": {
"type": "object",
"properties": {
"date": {
"description": "查询的日期,默认当天",
"type": "string"
},
"needDesc": {
"description": "是否需要返回当日公众日、国际日和我国传统节日的简介1-返回,默认不返回",
"type": "string"
}
}
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"description": "返回码"
},
"msg": {
"type": "string",
"description": "返回消息"
},
"taskNo": {
"type": "string",
"description": "请求号"
},
"data": {
"type": "object",
"properties": {
"day": {
"type": "string",
"description": "查询的日期"
},
"holiday": {
"type": "string",
"description": "节日名称"
},
"type": {
"type": "string",
"description": "日期类型"
},
"begin": {
"type": "string",
"description": "节日或周末开始时间"
},
"end": {
"type": "string",
"description": "节日或周末结束时间"
},
"holiday_remark": {
"type": "string",
"description": "节日备注"
},
"weekDay": {
"type": "integer",
"description": "星期几的数字"
},
"cn": {
"type": "string",
"description": "星期几的中文名"
},
"en": {
"type": "string",
"description": "星期几的英文名"
},
"h": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "节日名称"
},
"genus": {
"type": "string",
"description": "节日种类"
},
"day": {
"type": "string",
"description": "节日公历日期"
},
"lunaDay": {
"type": "string"
},
"info": {
"type": "string"
},
"origin": {
"type": "string"
}
}
}
}
}
}
}
}
}
},
"description": "成功响应"
}
}
}
}
},
"servers": [
{
"url": "https://jmhlysjjr.market.alicloudapi.com"
}
]
}

Some files were not shown because too many files have changed in this diff Show More