Просмотр исходного кода

feat: add bing cn search mcp server (#243)

* feat: add bing cn search mcp server

* chore: bump swag
zijiren 6 месяцев назад
Родитель
Сommit
9d87b5dedd

+ 3 - 0
core/controller/mcp/embedmcp.go

@@ -50,6 +50,7 @@ type EmbedMCP struct {
 	Enabled         bool                    `json:"enabled"`
 	Name            string                  `json:"name"`
 	Readme          string                  `json:"readme"`
+	GitHubURL       string                  `json:"github_url"`
 	Tags            []string                `json:"tags"`
 	ConfigTemplates EmbedMCPConfigTemplates `json:"config_templates"`
 }
@@ -60,6 +61,7 @@ func newEmbedMCP(mcp *mcpservers.McpServer, enabled bool) *EmbedMCP {
 		Enabled:         enabled,
 		Name:            mcp.Name,
 		Readme:          mcp.Readme,
+		GitHubURL:       mcp.GitHubURL,
 		Tags:            mcp.Tags,
 		ConfigTemplates: newEmbedMCPConfigTemplates(mcp.ConfigTemplates),
 	}
@@ -153,6 +155,7 @@ func ToPublicMCP(
 		Name:        e.Name,
 		LogoURL:     e.LogoURL,
 		Readme:      e.Readme,
+		GithubURL:   e.GitHubURL,
 		Tags:        e.Tags,
 		EmbedConfig: embedConfig,
 	}

+ 9 - 6
core/docs/docs.go

@@ -8132,6 +8132,9 @@ const docTemplate = `{
                 "enabled": {
                     "type": "boolean"
                 },
+                "github_url": {
+                    "type": "string"
+                },
                 "id": {
                     "type": "string"
                 },
@@ -8377,6 +8380,9 @@ const docTemplate = `{
                 "endpoints": {
                     "$ref": "#/definitions/controller.MCPEndpoint"
                 },
+                "github_url": {
+                    "type": "string"
+                },
                 "id": {
                     "type": "string"
                 },
@@ -8401,9 +8407,6 @@ const docTemplate = `{
                 "readme_url": {
                     "type": "string"
                 },
-                "repo_url": {
-                    "type": "string"
-                },
                 "status": {
                     "$ref": "#/definitions/model.PublicMCPStatus"
                 },
@@ -10218,6 +10221,9 @@ const docTemplate = `{
                 "embed_config": {
                     "$ref": "#/definitions/model.MCPEmbeddingConfig"
                 },
+                "github_url": {
+                    "type": "string"
+                },
                 "id": {
                     "type": "string"
                 },
@@ -10242,9 +10248,6 @@ const docTemplate = `{
                 "readme_url": {
                     "type": "string"
                 },
-                "repo_url": {
-                    "type": "string"
-                },
                 "status": {
                     "$ref": "#/definitions/model.PublicMCPStatus"
                 },

+ 9 - 6
core/docs/swagger.json

@@ -8123,6 +8123,9 @@
                 "enabled": {
                     "type": "boolean"
                 },
+                "github_url": {
+                    "type": "string"
+                },
                 "id": {
                     "type": "string"
                 },
@@ -8368,6 +8371,9 @@
                 "endpoints": {
                     "$ref": "#/definitions/controller.MCPEndpoint"
                 },
+                "github_url": {
+                    "type": "string"
+                },
                 "id": {
                     "type": "string"
                 },
@@ -8392,9 +8398,6 @@
                 "readme_url": {
                     "type": "string"
                 },
-                "repo_url": {
-                    "type": "string"
-                },
                 "status": {
                     "$ref": "#/definitions/model.PublicMCPStatus"
                 },
@@ -10209,6 +10212,9 @@
                 "embed_config": {
                     "$ref": "#/definitions/model.MCPEmbeddingConfig"
                 },
+                "github_url": {
+                    "type": "string"
+                },
                 "id": {
                     "type": "string"
                 },
@@ -10233,9 +10239,6 @@
                 "readme_url": {
                     "type": "string"
                 },
-                "repo_url": {
-                    "type": "string"
-                },
                 "status": {
                     "$ref": "#/definitions/model.PublicMCPStatus"
                 },

+ 6 - 4
core/docs/swagger.yaml

@@ -159,6 +159,8 @@ definitions:
         $ref: '#/definitions/controller.EmbedMCPConfigTemplates'
       enabled:
         type: boolean
+      github_url:
+        type: string
       id:
         type: string
       name:
@@ -319,6 +321,8 @@ definitions:
         $ref: '#/definitions/model.MCPEmbeddingConfig'
       endpoints:
         $ref: '#/definitions/controller.MCPEndpoint'
+      github_url:
+        type: string
       id:
         type: string
       logo_url:
@@ -335,8 +339,6 @@ definitions:
         type: string
       readme_url:
         type: string
-      repo_url:
-        type: string
       status:
         $ref: '#/definitions/model.PublicMCPStatus'
       tags:
@@ -1599,6 +1601,8 @@ definitions:
         type: string
       embed_config:
         $ref: '#/definitions/model.MCPEmbeddingConfig'
+      github_url:
+        type: string
       id:
         type: string
       logo_url:
@@ -1615,8 +1619,6 @@ definitions:
         type: string
       readme_url:
         type: string
-      repo_url:
-        type: string
       status:
         $ref: '#/definitions/model.PublicMCPStatus'
       tags:

+ 2 - 2
core/model/publicmcp.go

@@ -132,7 +132,7 @@ type PublicMCP struct {
 	PublicMCPReusingParams []PublicMCPReusingParam `gorm:"foreignKey:MCPID"              json:"-"`
 	Name                   string                  `                                     json:"name"`
 	Type                   PublicMCPType           `gorm:"index"                         json:"type"`
-	RepoURL                string                  `                                     json:"repo_url"`
+	GithubURL              string                  `                                     json:"github_url"`
 	ReadmeURL              string                  `                                     json:"readme_url"`
 	Readme                 string                  `gorm:"type:text"                     json:"readme"`
 	Tags                   []string                `gorm:"serializer:fastjson;type:text" json:"tags,omitempty"`
@@ -226,7 +226,7 @@ func UpdatePublicMCP(mcp *PublicMCP) (err error) {
 	}()
 
 	selects := []string{
-		"repo_url",
+		"github_url",
 		"readme",
 		"readme_url",
 		"tags",

+ 2 - 0
core/relay/plugin/web-search/search.go

@@ -290,6 +290,8 @@ func (p *WebSearch) initializeSearchEngines(configs []EngineConfig) ([]engine.En
 				return nil, false, err
 			}
 			engines = append(engines, engine.NewBingEngine(spec.APIKey))
+		case "bingcn":
+			engines = append(engines, engine.NewBingCNEngine())
 		case "google":
 			var spec GoogleSpec
 			if err := e.LoadSpec(&spec); err != nil {

+ 14 - 0
mcp-servers/bingcn/README.md

@@ -0,0 +1,14 @@
+# Bing CN MCP
+
+> <https://github.com/yan5236/bing-cn-mcp-server>
+
+一个基于 MCP (Model Context Protocol) 的中文必应搜索工具,可以直接通过 Claude 或其他支持 MCP 的 AI 来搜索必应并获取网页内容。
+
+## 特点
+
+- 支持中文搜索结果
+- 无需 API 密钥,直接爬取必应搜索结果
+- 提供网页内容获取功能
+- 轻量级,易于安装和使用
+- 专为中文用户优化
+- 支持 Claude 等 AI 工具调用

+ 240 - 0
mcp-servers/bingcn/bingcn.go

@@ -0,0 +1,240 @@
+package bingcn
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"strconv"
+	"time"
+
+	"github.com/bytedance/sonic"
+	mcpservers "github.com/labring/aiproxy/mcp-servers"
+	"github.com/mark3labs/mcp-go/mcp"
+	"github.com/mark3labs/mcp-go/server"
+)
+
+// BingSearchServer represents the MCP server for Bing search
+type BingSearchServer struct {
+	*server.MCPServer
+	searchEngine   *SearchEngine
+	webpageFetcher *WebpageFetcher
+}
+
+// Configuration templates
+var configTemplates = mcpservers.ConfigTemplates{
+	"user-agent": {
+		Name:        "User Agent",
+		Required:    mcpservers.ConfigRequiredTypeInitOptional,
+		Example:     DefaultUserAgent,
+		Description: "Custom User-Agent string to use for requests",
+	},
+	"timeout": {
+		Name:        "Request Timeout",
+		Required:    mcpservers.ConfigRequiredTypeInitOptional,
+		Example:     "15",
+		Description: "Request timeout in seconds (default: 15)",
+		Validator: func(value string) error {
+			timeout, err := strconv.Atoi(value)
+			if err != nil {
+				return errors.New("timeout must be a number")
+			}
+			if timeout < 1 || timeout > 60 {
+				return errors.New("timeout must be between 1 and 60 seconds")
+			}
+			return nil
+		},
+	},
+}
+
+// NewServer creates a new Bing search MCP server
+func NewServer(config, _ map[string]string) (mcpservers.Server, error) {
+	userAgent := config["user-agent"]
+	if userAgent == "" {
+		userAgent = DefaultUserAgent
+	}
+
+	timeout := 15 * time.Second
+	if timeoutStr := config["timeout"]; timeoutStr != "" {
+		if t, err := strconv.Atoi(timeoutStr); err == nil {
+			timeout = time.Duration(t) * time.Second
+		}
+	}
+
+	// Create MCP server
+	mcpServer := server.NewMCPServer("bing-search", "1.0.0")
+
+	// Create search engine and webpage fetcher
+	searchEngine := NewSearchEngine(userAgent, timeout)
+	webpageFetcher := NewWebpageFetcher(userAgent, searchEngine.client)
+
+	bingServer := &BingSearchServer{
+		MCPServer:      mcpServer,
+		searchEngine:   searchEngine,
+		webpageFetcher: webpageFetcher,
+	}
+
+	bingServer.addTools()
+
+	return bingServer, nil
+}
+
+// addTools adds the search and fetch tools to the server
+func (s *BingSearchServer) addTools() {
+	s.addBingSearchTool()
+	s.addFetchWebpageTool()
+}
+
+// addBingSearchTool adds the Bing search tool
+func (s *BingSearchServer) addBingSearchTool() {
+	s.AddTool(
+		mcp.Tool{
+			Name:        "bing_search",
+			Description: "使用必应搜索指定的关键词,并返回搜索结果列表,包括标题、链接、摘要和ID",
+			InputSchema: mcp.ToolInputSchema{
+				Type: "object",
+				Properties: map[string]any{
+					"query": map[string]any{
+						"type":        "string",
+						"description": "搜索关键词",
+					},
+					"num_results": map[string]any{
+						"type":        "integer",
+						"description": "返回的结果数量,默认为5",
+						"default":     5,
+						"minimum":     1,
+						"maximum":     20,
+					},
+					"language": map[string]any{
+						"type":        "string",
+						"description": "搜索语言设置,如 'zh-CN', 'en-US'",
+						"default":     "zh-CN",
+					},
+				},
+				Required: []string{"query"},
+			},
+		},
+		s.handleBingSearch,
+	)
+}
+
+// addFetchWebpageTool adds the webpage fetch tool
+func (s *BingSearchServer) addFetchWebpageTool() {
+	s.AddTool(
+		mcp.Tool{
+			Name:        "fetch_webpage",
+			Description: "根据提供的ID获取对应网页的内容",
+			InputSchema: mcp.ToolInputSchema{
+				Type: "object",
+				Properties: map[string]any{
+					"result_id": map[string]any{
+						"type":        "string",
+						"description": "从bing_search返回的结果ID",
+					},
+					"max_length": map[string]any{
+						"type":        "integer",
+						"description": "返回内容的最大长度",
+						"default":     8000,
+						"minimum":     100,
+						"maximum":     50000,
+					},
+				},
+				Required: []string{"result_id"},
+			},
+		},
+		s.handleFetchWebpage,
+	)
+}
+
+// handleBingSearch handles the Bing search tool
+func (s *BingSearchServer) handleBingSearch(
+	ctx context.Context,
+	request mcp.CallToolRequest,
+) (*mcp.CallToolResult, error) {
+	args := request.GetArguments()
+
+	query, ok := args["query"].(string)
+	if !ok || query == "" {
+		return nil, errors.New("query is required")
+	}
+
+	numResults := 5
+	if nr, ok := args["num_results"].(float64); ok {
+		numResults = int(nr)
+	}
+
+	language := "zh-CN"
+	if lang, ok := args["language"].(string); ok && lang != "" {
+		language = lang
+	}
+
+	// Perform search using search engine
+	results, err := s.searchEngine.Search(ctx, SearchOptions{
+		Query:      query,
+		NumResults: numResults,
+		Language:   language,
+	})
+	if err != nil {
+		return nil, fmt.Errorf("search failed: %w", err)
+	}
+
+	// Format response
+	response := map[string]any{
+		"query":    query,
+		"language": language,
+		"count":    len(results),
+		"results":  results,
+	}
+
+	responseJSON, err := sonic.Marshal(response)
+	if err != nil {
+		return nil, fmt.Errorf("failed to marshal response: %w", err)
+	}
+
+	return mcp.NewToolResultText(string(responseJSON)), nil
+}
+
+// handleFetchWebpage handles the webpage fetch tool
+func (s *BingSearchServer) handleFetchWebpage(
+	ctx context.Context,
+	request mcp.CallToolRequest,
+) (*mcp.CallToolResult, error) {
+	args := request.GetArguments()
+
+	resultID, ok := args["result_id"].(string)
+	if !ok || resultID == "" {
+		return nil, errors.New("result_id is required")
+	}
+
+	maxLength := 8000
+	if ml, ok := args["max_length"].(float64); ok {
+		maxLength = int(ml)
+	}
+
+	// Get search result from engine
+	result, exists := s.searchEngine.GetSearchResult(resultID)
+	if !exists {
+		return nil, fmt.Errorf("找不到ID为 %s 的搜索结果", resultID)
+	}
+
+	// Fetch webpage content using webpage fetcher
+	content, err := s.webpageFetcher.FetchWebpageByResult(ctx, result, maxLength)
+	if err != nil {
+		return nil, fmt.Errorf("failed to fetch webpage: %w", err)
+	}
+
+	// Format response
+	response := map[string]any{
+		"result_id": resultID,
+		"url":       content.URL,
+		"title":     content.Title,
+		"content":   content.Content,
+		"length":    content.Length,
+	}
+
+	responseJSON, err := sonic.Marshal(response)
+	if err != nil {
+		return nil, fmt.Errorf("failed to marshal response: %w", err)
+	}
+
+	return mcp.NewToolResultText(string(responseJSON)), nil
+}

+ 333 - 0
mcp-servers/bingcn/engine.go

@@ -0,0 +1,333 @@
+package bingcn
+
+import (
+	"context"
+	"crypto/tls"
+	"errors"
+	"fmt"
+	"io"
+	"net/http"
+	"net/url"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/PuerkitoBio/goquery"
+	"golang.org/x/text/encoding/simplifiedchinese"
+	"golang.org/x/text/transform"
+)
+
+type SearchResult struct {
+	ID      string `json:"id"`
+	Title   string `json:"title"`
+	Link    string `json:"link"`
+	Snippet string `json:"snippet"`
+}
+
+const (
+	DefaultUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
+)
+
+// SearchEngine represents a Bing search engine
+type SearchEngine struct {
+	userAgent     string
+	client        *http.Client
+	searchResults sync.Map // map[string]*SearchResult
+}
+
+// SearchOptions contains options for search
+type SearchOptions struct {
+	Query      string
+	NumResults int
+	Language   string
+}
+
+// NewSearchEngine creates a new search engine instance
+func NewSearchEngine(userAgent string, timeout time.Duration) *SearchEngine {
+	if userAgent == "" {
+		userAgent = DefaultUserAgent
+	}
+
+	client := &http.Client{
+		Timeout: timeout,
+		Transport: &http.Transport{
+			TLSClientConfig: &tls.Config{
+				InsecureSkipVerify: false,
+				MinVersion:         tls.VersionTLS12,
+			},
+		},
+	}
+
+	return &SearchEngine{
+		userAgent: userAgent,
+		client:    client,
+	}
+}
+
+// Search performs a Bing search and returns results
+func (e *SearchEngine) Search(ctx context.Context, options SearchOptions) ([]*SearchResult, error) {
+	if options.Query == "" {
+		return nil, errors.New("query cannot be empty")
+	}
+
+	if options.NumResults <= 0 {
+		options.NumResults = 5
+	}
+
+	// Build search URL
+	searchURL := e.buildSearchURL(options.Query, options.Language)
+
+	// Create and execute request
+	req, err := http.NewRequestWithContext(ctx, http.MethodGet, searchURL, nil)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create request: %w", err)
+	}
+
+	e.setSearchHeaders(req)
+
+	resp, err := e.client.Do(req)
+	if err != nil {
+		return nil, fmt.Errorf("failed to execute request: %w", err)
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		return nil, fmt.Errorf("search request failed with status: %d", resp.StatusCode)
+	}
+
+	// Read and decode response
+	body, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, fmt.Errorf("failed to read response body: %w", err)
+	}
+
+	content := e.decodeContent(body, resp.Header.Get("Content-Type"))
+
+	// Parse HTML
+	doc, err := goquery.NewDocumentFromReader(strings.NewReader(content))
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse HTML: %w", err)
+	}
+
+	// Extract results
+	return e.extractSearchResults(doc, options.Query, options.NumResults), nil
+}
+
+// GetSearchResult retrieves a stored search result by ID
+func (e *SearchEngine) GetSearchResult(resultID string) (*SearchResult, bool) {
+	value, ok := e.searchResults.Load(resultID)
+	if !ok {
+		return nil, false
+	}
+
+	result, ok := value.(*SearchResult)
+	return result, ok
+}
+
+// buildSearchURL constructs the Bing search URL
+func (e *SearchEngine) buildSearchURL(query, language string) string {
+	baseURL := "https://cn.bing.com/search"
+	params := url.Values{}
+	params.Set("q", query)
+	params.Set("setlang", "zh-CN")
+	params.Set("ensearch", "0")
+
+	if language != "" {
+		params.Set("setlang", language)
+	}
+
+	return fmt.Sprintf("%s?%s", baseURL, params.Encode())
+}
+
+// setSearchHeaders sets appropriate headers for search requests
+func (e *SearchEngine) setSearchHeaders(req *http.Request) {
+	headers := map[string]string{
+		"User-Agent":                e.userAgent,
+		"Accept":                    "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
+		"Accept-Language":           "zh-CN,zh;q=0.9,en;q=0.8",
+		"Cache-Control":             "no-cache",
+		"Pragma":                    "no-cache",
+		"Sec-Fetch-Dest":            "document",
+		"Sec-Fetch-Mode":            "navigate",
+		"Sec-Fetch-Site":            "none",
+		"Sec-Fetch-User":            "?1",
+		"Upgrade-Insecure-Requests": "1",
+		"Cookie":                    "SRCHHPGUSR=SRCHLANG=zh-Hans; _EDGE_S=ui=zh-cn; _EDGE_V=1",
+	}
+
+	for key, value := range headers {
+		req.Header.Set(key, value)
+	}
+}
+
+// decodeContent attempts to properly decode content based on encoding
+func (e *SearchEngine) decodeContent(body []byte, contentType string) string {
+	content := string(body)
+
+	// Check if content type suggests GBK encoding
+	if strings.Contains(strings.ToLower(contentType), "gbk") ||
+		strings.Contains(strings.ToLower(contentType), "gb2312") {
+		if decoded, err := e.decodeGBK(body); err == nil {
+			content = decoded
+		}
+	}
+
+	return content
+}
+
+// decodeGBK decodes GBK encoded content to UTF-8
+func (e *SearchEngine) decodeGBK(data []byte) (string, error) {
+	reader := transform.NewReader(
+		strings.NewReader(string(data)),
+		simplifiedchinese.GBK.NewDecoder(),
+	)
+	decoded, err := io.ReadAll(reader)
+	if err != nil {
+		return "", err
+	}
+	return string(decoded), nil
+}
+
+// extractSearchResults extracts search results from parsed HTML
+func (e *SearchEngine) extractSearchResults(
+	doc *goquery.Document,
+	query string,
+	numResults int,
+) []*SearchResult {
+	var results []*SearchResult
+
+	// Try different selectors for Bing search results
+	selectors := []string{
+		"#b_results > li.b_algo",
+		"#b_results > .b_ans",
+		"#b_results > li",
+	}
+
+	for _, selector := range selectors {
+		doc.Find(selector).Each(func(i int, element *goquery.Selection) {
+			if len(results) >= numResults {
+				return
+			}
+
+			result := e.parseSearchResultElement(element, i)
+			if result != nil {
+				// Store result for later retrieval
+				e.searchResults.Store(result.ID, result)
+				results = append(results, result)
+			}
+		})
+
+		// If we found results with this selector, stop trying others
+		if len(results) > 0 {
+			break
+		}
+	}
+
+	// If no results found, create a fallback result
+	if len(results) == 0 {
+		fallbackResult := e.createFallbackResult(query)
+		e.searchResults.Store(fallbackResult.ID, fallbackResult)
+		results = append(results, fallbackResult)
+	}
+
+	return results
+}
+
+// parseSearchResultElement parses a single search result element
+func (e *SearchEngine) parseSearchResultElement(
+	element *goquery.Selection,
+	index int,
+) *SearchResult {
+	// Skip ads
+	if element.HasClass("b_ad") {
+		return nil
+	}
+
+	title, link := e.extractTitleAndLink(element)
+	snippet := e.extractSnippet(element, title)
+
+	// Fix incomplete links
+	if link != "" && !strings.HasPrefix(link, "http") {
+		link = e.fixIncompleteLink(link)
+	}
+
+	// Skip if no meaningful content
+	if title == "" && snippet == "" {
+		return nil
+	}
+
+	// Create unique ID
+	id := fmt.Sprintf("result_%d_%d", time.Now().UnixNano(), index)
+
+	return &SearchResult{
+		ID:      id,
+		Title:   title,
+		Link:    link,
+		Snippet: snippet,
+	}
+}
+
+// extractTitleAndLink extracts title and link from a search result element
+func (e *SearchEngine) extractTitleAndLink(element *goquery.Selection) (string, string) {
+	// Try to find title and link in h2 a
+	titleElement := element.Find("h2 a").First()
+	if titleElement.Length() > 0 {
+		title := strings.TrimSpace(titleElement.Text())
+		link, _ := titleElement.Attr("href")
+		return title, link
+	}
+
+	// Try alternative selectors
+	altTitleElement := element.Find(".b_title a, a.tilk, a strong").First()
+	if altTitleElement.Length() > 0 {
+		title := strings.TrimSpace(altTitleElement.Text())
+		link, _ := altTitleElement.Attr("href")
+		return title, link
+	}
+
+	return "", ""
+}
+
+// extractSnippet extracts snippet from a search result element
+func (e *SearchEngine) extractSnippet(element *goquery.Selection, title string) string {
+	// Try to find snippet in common Bing snippet selectors
+	snippetElement := element.Find(".b_caption p, .b_snippet, .b_algoSlug").First()
+	if snippetElement.Length() > 0 {
+		return strings.TrimSpace(snippetElement.Text())
+	}
+
+	// If no snippet found, use entire element text and clean it up
+	snippet := strings.TrimSpace(element.Text())
+
+	// Remove title from snippet
+	if title != "" && strings.Contains(snippet, title) {
+		snippet = strings.ReplaceAll(snippet, title, "")
+		snippet = strings.TrimSpace(snippet)
+	}
+
+	// Truncate if too long
+	if len(snippet) > 150 {
+		snippet = snippet[:150] + "..."
+	}
+
+	return snippet
+}
+
+// fixIncompleteLink fixes incomplete URLs
+func (e *SearchEngine) fixIncompleteLink(link string) string {
+	if strings.HasPrefix(link, "/") {
+		return "https://cn.bing.com" + link
+	}
+	return "https://cn.bing.com/" + link
+}
+
+// createFallbackResult creates a fallback result when no results are found
+func (e *SearchEngine) createFallbackResult(query string) *SearchResult {
+	id := fmt.Sprintf("result_%d_fallback", time.Now().UnixNano())
+	return &SearchResult{
+		ID:      id,
+		Title:   "搜索结果: " + query,
+		Link:    "https://cn.bing.com/search?q=" + url.QueryEscape(query),
+		Snippet: fmt.Sprintf("未能解析关于 \"%s\" 的搜索结果,但您可以直接访问必应搜索页面查看。", query),
+	}
+}

+ 27 - 0
mcp-servers/bingcn/init.go

@@ -0,0 +1,27 @@
+package bingcn
+
+import (
+	_ "embed"
+
+	mcpservers "github.com/labring/aiproxy/mcp-servers"
+)
+
+//go:embed README.md
+var readme string
+
+func init() {
+	mcpservers.Register(
+		mcpservers.NewMcp(
+			"bing-cn-search",
+			"Bing CN Search",
+			mcpservers.McpTypeEmbed,
+			mcpservers.WithNewServerFunc(NewServer),
+			mcpservers.WithGitHubURL(
+				"https://github.com/yan5236/bing-cn-mcp-server",
+			),
+			mcpservers.WithConfigTemplates(configTemplates),
+			mcpservers.WithTags([]string{"search", "bing", "web", "scraping"}),
+			mcpservers.WithReadme(readme),
+		),
+	)
+}

+ 265 - 0
mcp-servers/bingcn/webpage_fetcher.go

@@ -0,0 +1,265 @@
+package bingcn
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"io"
+	"net/http"
+	"strings"
+
+	"github.com/PuerkitoBio/goquery"
+)
+
+// WebpageFetcher handles fetching and extracting content from webpages
+type WebpageFetcher struct {
+	userAgent string
+	client    *http.Client
+}
+
+// FetchOptions contains options for webpage fetching
+type FetchOptions struct {
+	URL         string
+	MaxLength   int
+	ExtractText bool
+}
+
+// WebpageContent represents extracted webpage content
+type WebpageContent struct {
+	Title   string
+	Content string
+	URL     string
+	Length  int
+}
+
+// NewWebpageFetcher creates a new webpage fetcher
+func NewWebpageFetcher(userAgent string, client *http.Client) *WebpageFetcher {
+	if userAgent == "" {
+		userAgent = DefaultUserAgent
+	}
+
+	return &WebpageFetcher{
+		userAgent: userAgent,
+		client:    client,
+	}
+}
+
+// FetchWebpage fetches and extracts content from a webpage
+func (f *WebpageFetcher) FetchWebpage(
+	ctx context.Context,
+	options FetchOptions,
+) (*WebpageContent, error) {
+	if options.URL == "" {
+		return nil, errors.New("URL cannot be empty")
+	}
+
+	if options.MaxLength <= 0 {
+		options.MaxLength = 8000
+	}
+
+	// Create request
+	req, err := http.NewRequestWithContext(ctx, http.MethodGet, options.URL, nil)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create request: %w", err)
+	}
+
+	f.setWebpageHeaders(req)
+
+	// Execute request
+	resp, err := f.client.Do(req)
+	if err != nil {
+		return nil, fmt.Errorf("failed to fetch webpage: %w", err)
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		return nil, fmt.Errorf("webpage request failed with status: %d", resp.StatusCode)
+	}
+
+	// Read response body
+	body, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, fmt.Errorf("failed to read response body: %w", err)
+	}
+
+	// Decode content
+	content := f.decodeContent(body, resp.Header.Get("Content-Type"))
+
+	// Extract webpage content
+	webpageContent, err := f.extractContent(content, options)
+	if err != nil {
+		return nil, fmt.Errorf("failed to extract content: %w", err)
+	}
+
+	webpageContent.URL = options.URL
+	return webpageContent, nil
+}
+
+// FetchWebpageByResult fetches webpage content using a search result
+func (f *WebpageFetcher) FetchWebpageByResult(
+	ctx context.Context,
+	result *SearchResult,
+	maxLength int,
+) (*WebpageContent, error) {
+	options := FetchOptions{
+		URL:         result.Link,
+		MaxLength:   maxLength,
+		ExtractText: true,
+	}
+
+	return f.FetchWebpage(ctx, options)
+}
+
+// setWebpageHeaders sets appropriate headers for webpage requests
+func (f *WebpageFetcher) setWebpageHeaders(req *http.Request) {
+	headers := map[string]string{
+		"User-Agent":      f.userAgent,
+		"Accept":          "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
+		"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
+		"Cache-Control":   "no-cache",
+		"Pragma":          "no-cache",
+		"Referer":         "https://cn.bing.com/",
+	}
+
+	for key, value := range headers {
+		req.Header.Set(key, value)
+	}
+}
+
+// decodeContent decodes content with proper encoding handling
+func (f *WebpageFetcher) decodeContent(body []byte, contentType string) string {
+	content := string(body)
+
+	// Check if content type suggests GBK encoding
+	if strings.Contains(strings.ToLower(contentType), "gbk") ||
+		strings.Contains(strings.ToLower(contentType), "gb2312") {
+		// Use the same decoding logic as SearchEngine
+		engine := &SearchEngine{}
+		if decoded, err := engine.decodeGBK(body); err == nil {
+			content = decoded
+		}
+	}
+
+	return content
+}
+
+// extractContent extracts and cleans the main content from HTML
+func (f *WebpageFetcher) extractContent(
+	htmlContent string,
+	options FetchOptions,
+) (*WebpageContent, error) {
+	doc, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse HTML: %w", err)
+	}
+
+	// Remove unwanted elements
+	f.removeUnwantedElements(doc)
+
+	// Extract title
+	title := strings.TrimSpace(doc.Find("title").Text())
+
+	var content string
+	if options.ExtractText {
+		content = f.extractMainContent(doc)
+	} else {
+		content = htmlContent
+	}
+
+	// Clean up the text
+	content = f.cleanText(content)
+
+	// Add title if available and extracting text
+	if title != "" && options.ExtractText {
+		content = fmt.Sprintf("标题: %s\n\n%s", title, content)
+	}
+
+	// Truncate if too long
+	if len(content) > options.MaxLength {
+		content = content[:options.MaxLength] + "... (内容已截断)"
+	}
+
+	return &WebpageContent{
+		Title:   title,
+		Content: content,
+		Length:  len(content),
+	}, nil
+}
+
+// removeUnwantedElements removes unwanted HTML elements
+func (f *WebpageFetcher) removeUnwantedElements(doc *goquery.Document) {
+	unwantedSelectors := []string{
+		"script", "style", "iframe", "noscript", "nav", "header", "footer",
+		".header", ".footer", ".nav", ".sidebar", ".ad", ".advertisement",
+		"#header", "#footer", "#nav", "#sidebar",
+	}
+
+	for _, selector := range unwantedSelectors {
+		doc.Find(selector).Remove()
+	}
+}
+
+// extractMainContent extracts the main content from the document
+func (f *WebpageFetcher) extractMainContent(doc *goquery.Document) string {
+	var content string
+
+	// Try to find main content areas
+	mainSelectors := []string{
+		"main", "article", ".article", ".post", ".content", "#content",
+		".main", "#main", ".body", "#body", ".entry", ".entry-content",
+		".post-content", ".article-content", ".text", ".detail",
+	}
+
+	for _, selector := range mainSelectors {
+		mainElement := doc.Find(selector)
+		if mainElement.Length() > 0 {
+			content = strings.TrimSpace(mainElement.Text())
+			if len(content) > 100 {
+				return content
+			}
+		}
+	}
+
+	// If no main content found, try paragraphs
+	if content == "" || len(content) < 100 {
+		content = f.extractParagraphs(doc)
+	}
+
+	// If still no content, get body content
+	if content == "" || len(content) < 100 {
+		content = strings.TrimSpace(doc.Find("body").Text())
+	}
+
+	return content
+}
+
+// extractParagraphs extracts meaningful paragraphs from the document
+func (f *WebpageFetcher) extractParagraphs(doc *goquery.Document) string {
+	var paragraphs []string
+	doc.Find("p").Each(func(_ int, element *goquery.Selection) {
+		text := strings.TrimSpace(element.Text())
+		if len(text) > 20 {
+			paragraphs = append(paragraphs, text)
+		}
+	})
+
+	if len(paragraphs) > 0 {
+		return strings.Join(paragraphs, "\n\n")
+	}
+
+	return ""
+}
+
+// cleanText cleans and normalizes extracted text
+func (f *WebpageFetcher) cleanText(text string) string {
+	// Replace newlines and tabs with spaces
+	text = strings.ReplaceAll(text, "\n", " ")
+	text = strings.ReplaceAll(text, "\t", " ")
+
+	// Replace multiple spaces with single space
+	for strings.Contains(text, "  ") {
+		text = strings.ReplaceAll(text, "  ", " ")
+	}
+
+	return strings.TrimSpace(text)
+}

+ 3 - 0
mcp-servers/fetch/init.go

@@ -16,6 +16,9 @@ func init() {
 			"Fetch",
 			mcpservers.McpTypeEmbed,
 			mcpservers.WithNewServerFunc(NewServer),
+			mcpservers.WithGitHubURL(
+				"https://github.com/modelcontextprotocol/servers/tree/main/src/fetch",
+			),
 			mcpservers.WithConfigTemplates(configTemplates),
 			mcpservers.WithTags([]string{"fetch", "web", "html", "markdown"}),
 			mcpservers.WithReadme(readme),

+ 2 - 2
mcp-servers/go.mod

@@ -4,17 +4,18 @@ go 1.24
 
 require (
 	github.com/JohannesKaufmann/html-to-markdown v1.6.0
+	github.com/PuerkitoBio/goquery v1.9.2
 	github.com/bytedance/sonic v1.13.2
 	github.com/go-shiori/go-readability v0.0.0-20250217085726-9f5bf5ca7612
 	github.com/labring/aiproxy/core v0.0.0-20250527101240-aac8e89068ad
 	github.com/labring/aiproxy/openapi-mcp v0.0.0-20250527101240-aac8e89068ad
 	github.com/mark3labs/mcp-go v0.30.0
 	github.com/temoto/robotstxt v1.1.2
+	golang.org/x/text v0.25.0
 )
 
 require (
 	github.com/KyleBanks/depth v1.2.1 // indirect
-	github.com/PuerkitoBio/goquery v1.9.2 // indirect
 	github.com/andybalholm/cascadia v1.3.3 // indirect
 	github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de // indirect
 	github.com/bytedance/sonic/loader v0.2.4 // indirect
@@ -42,7 +43,6 @@ require (
 	golang.org/x/arch v0.17.0 // indirect
 	golang.org/x/net v0.40.0 // indirect
 	golang.org/x/sys v0.33.0 // indirect
-	golang.org/x/text v0.25.0 // indirect
 	golang.org/x/tools v0.33.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )

+ 7 - 0
mcp-servers/mcp.go

@@ -111,6 +111,7 @@ type McpServer struct {
 	ID              string
 	Name            string
 	Type            McpType
+	GitHubURL       string
 	Readme          string
 	LogoURL         string
 	Tags            []string
@@ -126,6 +127,12 @@ func WithReadme(readme string) McpConfig {
 	}
 }
 
+func WithGitHubURL(githubURL string) McpConfig {
+	return func(e *McpServer) {
+		e.GitHubURL = githubURL
+	}
+}
+
 func WithType(t McpType) McpConfig {
 	return func(e *McpServer) {
 		e.Type = t

+ 1 - 0
mcp-servers/mcpregister/init.go

@@ -5,6 +5,7 @@ import (
 	_ "github.com/labring/aiproxy/mcp-servers/aiproxy-openapi"
 	_ "github.com/labring/aiproxy/mcp-servers/alipay"
 	_ "github.com/labring/aiproxy/mcp-servers/amap"
+	_ "github.com/labring/aiproxy/mcp-servers/bingcn"
 	_ "github.com/labring/aiproxy/mcp-servers/fetch"
 	_ "github.com/labring/aiproxy/mcp-servers/web-search"
 )

+ 6 - 2
mcp-servers/web-search/README.md

@@ -1,10 +1,10 @@
 # Web Search MCP Server
 
-A comprehensive web search MCP server that provides access to multiple search engines including Google, Bing, and Arxiv.
+A comprehensive web search MCP server that provides access to multiple search engines including Google, Bing, Bing CN(Free), and Arxiv.
 
 ## Features
 
-- **Multiple Search Engines**: Integrated support for Google, Bing, and Arxiv
+- **Multiple Search Engines**: Integrated support for Google, Bing, Bing CN(Free), and Arxiv
 - **Flexible Configuration**: Configure only the search engines you need
 - **Multi-Engine Search**: Search across multiple engines simultaneously
 - **Smart Search**: Intelligent query optimization and result aggregation
@@ -27,6 +27,10 @@ At least one search engine must be configured with valid API credentials:
 
 - `bing_api_key`: Your Bing Search API key
 
+#### Bing CN Search
+
+Free, no API key required.
+
 #### Arxiv Search
 
 No configuration required - Arxiv is free to use.

+ 46 - 0
mcp-servers/web-search/engine/bingcn.go

@@ -0,0 +1,46 @@
+package engine
+
+import (
+	"context"
+	"strings"
+	"time"
+
+	"github.com/labring/aiproxy/mcp-servers/bingcn"
+)
+
+type BingCNEngine struct {
+	bingcn *bingcn.SearchEngine
+}
+
+func NewBingCNEngine() *BingCNEngine {
+	return &BingCNEngine{
+		bingcn: bingcn.NewSearchEngine("", 10*time.Second),
+	}
+}
+
+func (e *BingCNEngine) Search(
+	ctx context.Context,
+	query SearchQuery,
+) ([]SearchResult, error) {
+	options := bingcn.SearchOptions{
+		Query:      strings.Join(query.Queries, " "),
+		NumResults: query.MaxResults,
+		Language:   query.Language,
+	}
+
+	results, err := e.bingcn.Search(ctx, options)
+	if err != nil {
+		return nil, err
+	}
+
+	searchResults := make([]SearchResult, 0, len(results))
+	for _, result := range results {
+		searchResults = append(searchResults, SearchResult{
+			Title:   result.Title,
+			Link:    result.Link,
+			Content: result.Snippet,
+		})
+	}
+
+	return searchResults, nil
+}

+ 4 - 3
mcp-servers/web-search/features.md

@@ -1,10 +1,10 @@
 # Web Search MCP Server
 
-This MCP server provides web search capabilities through multiple search engines including Google, Bing, and Arxiv.
+This MCP server provides web search capabilities through multiple search engines including Google, Bing, Bing CN(Free), and Arxiv.
 
 ## Features
 
-- **Multiple Search Engines**: Support for Google, Bing, and Arxiv
+- **Multiple Search Engines**: Support for Google, Bing, Bing CN(Free), and Arxiv
 - **Multi-Engine Search**: Search across multiple engines simultaneously
 - **Smart Search**: Intelligent query optimization and result aggregation
 - **Academic Search**: Specialized support for academic papers via Arxiv
@@ -17,6 +17,7 @@ Configure the search engines you want to use by providing their API keys:
 
 - **Google**: Requires both API key and Custom Search Engine ID
 - **Bing**: Requires Bing Search API key
+- **Bing CN**: Free, no API key required
 - **Arxiv**: No API key required (free to use)
 - **SearchXNG**: No API key required (free to use)
 - **SearchXNG Base URL**: Base URL for SearchXNG
@@ -49,5 +50,5 @@ Intelligently optimize queries and aggregate results for better answers.
 
 3. Multi-engine search:
    - Query: "climate change impacts"
-   - Engines: ["google", "bing", "arxiv", "searchxng"]
+   - Engines: ["google", "bing", "arxiv", "searchxng", "bingcn"]
    - Max results per engine: 5

+ 9 - 1
mcp-servers/web-search/server.go

@@ -49,7 +49,7 @@ var configTemplates = map[string]mcpservers.ConfigTemplate{
 		Example:     "google",
 		Description: "Default search engine to use (google, bing, arxiv)",
 		Validator: func(value string) error {
-			validEngines := []string{"google", "bing", "arxiv", "searchxng"}
+			validEngines := []string{"google", "bing", "arxiv", "searchxng", "bingcn"}
 			for _, e := range validEngines {
 				if value == e {
 					return nil
@@ -133,6 +133,9 @@ func initializeEngines(config map[string]string) (map[string]engine.Engine, stri
 		engines["bing"] = engine.NewBingEngine(apiKey)
 	}
 
+	// Bing CN Search
+	engines["bingcn"] = engine.NewBingCNEngine()
+
 	// Arxiv is always available (no API key required)
 	engines["arxiv"] = engine.NewArxivEngine()
 
@@ -567,6 +570,11 @@ func determineEngine(q searchQuery, engines map[string]engine.Engine, includeAca
 		return "bing"
 	}
 
+	// Then Bing CN
+	if _, ok := engines["bingcn"]; ok {
+		return "bingcn"
+	}
+
 	// Then SearchXNG
 	if _, ok := engines["searchxng"]; ok {
 		return "searchxng"

+ 0 - 1853
web/mcp.txt

@@ -1,1853 +0,0 @@
-File: core/controller/mcp/publicmcp.go
-```go
-package controller
-
-import (
-	"fmt"
-	"net/http"
-	"strconv"
-
-	"github.com/bytedance/sonic"
-	"github.com/gin-gonic/gin"
-	"github.com/labring/aiproxy/core/common/config"
-	"github.com/labring/aiproxy/core/controller/utils"
-	"github.com/labring/aiproxy/core/middleware"
-	"github.com/labring/aiproxy/core/model"
-)
-
-type MCPEndpoint struct {
-	Host           string `json:"host"`
-	SSE            string `json:"sse"`
-	StreamableHTTP string `json:"streamable_http"`
-}
-
-type PublicMCPResponse struct {
-	model.PublicMCP
-	Endpoints MCPEndpoint `json:"endpoints"`
-}
-
-func (mcp *PublicMCPResponse) MarshalJSON() ([]byte, error) {
-	type Alias PublicMCPResponse
-	a := &struct {
-		*Alias
-		CreatedAt int64 `json:"created_at"`
-		UpdateAt  int64 `json:"update_at"`
-	}{
-		Alias:     (*Alias)(mcp),
-		CreatedAt: mcp.CreatedAt.UnixMilli(),
-		UpdateAt:  mcp.UpdateAt.UnixMilli(),
-	}
-	return sonic.Marshal(a)
-}
-
-func NewPublicMCPResponse(host string, mcp model.PublicMCP) PublicMCPResponse {
-	ep := MCPEndpoint{}
-	switch mcp.Type {
-	case model.PublicMCPTypeProxySSE,
-		model.PublicMCPTypeProxyStreamable,
-		model.PublicMCPTypeEmbed,
-		model.PublicMCPTypeOpenAPI:
-		publicMCPHost := config.GetPublicMCPHost()
-		if publicMCPHost == "" {
-			ep.Host = host
-			ep.SSE = fmt.Sprintf("/mcp/public/%s/sse", mcp.ID)
-			ep.StreamableHTTP = "/mcp/public/" + mcp.ID
-		} else {
-			ep.Host = fmt.Sprintf("%s.%s", mcp.ID, publicMCPHost)
-			ep.SSE = "/sse"
-			ep.StreamableHTTP = "/mcp"
-		}
-	case model.PublicMCPTypeDocs:
-	}
-	return PublicMCPResponse{
-		PublicMCP: mcp,
-		Endpoints: ep,
-	}
-}
-
-func NewPublicMCPResponses(host string, mcps []model.PublicMCP) []PublicMCPResponse {
-	responses := make([]PublicMCPResponse, len(mcps))
-	for i, mcp := range mcps {
-		responses[i] = NewPublicMCPResponse(host, mcp)
-	}
-	return responses
-}
-
-// GetPublicMCPs godoc
-//
-//	@Summary		Get MCPs
-//	@Description	Get a list of MCPs with pagination and filtering
-//	@Tags			mcp
-//	@Produce		json
-//	@Security		ApiKeyAuth
-//	@Param			page		query		int		false	"Page number"
-//	@Param			per_page	query		int		false	"Items per page"
-//	@Param			type		query		string	false	"MCP type"
-//	@Param			keyword		query		string	false	"Search keyword"
-//	@Param			status		query		int		false	"MCP status"
-//	@Success		200			{object}	middleware.APIResponse{data=[]PublicMCPResponse}
-//	@Router			/api/mcp/public/ [get]
-func GetPublicMCPs(c *gin.Context) {
-	page, perPage := utils.ParsePageParams(c)
-	mcpType := model.PublicMCPType(c.Query("type"))
-	keyword := c.Query("keyword")
-	status, _ := strconv.Atoi(c.Query("status"))
-
-	if status == 0 {
-		status = int(model.PublicMCPStatusEnabled)
-	}
-
-	mcps, total, err := model.GetPublicMCPs(
-		page,
-		perPage,
-		mcpType,
-		keyword,
-		model.PublicMCPStatus(status),
-	)
-	if err != nil {
-		middleware.ErrorResponse(c, http.StatusInternalServerError, err.Error())
-		return
-	}
-
-	middleware.SuccessResponse(c, gin.H{
-		"mcps":  NewPublicMCPResponses(c.Request.Host, mcps),
-		"total": total,
-	})
-}
-
-// GetAllPublicMCPs godoc
-//
-//	@Summary		Get all MCPs
-//	@Description	Get all MCPs with filtering
-//	@Tags			mcp
-//	@Produce		json
-//	@Security		ApiKeyAuth
-//	@Param			status	query		int	false	"MCP status"
-//	@Success		200		{object}	middleware.APIResponse{data=[]PublicMCPResponse}
-//	@Router			/api/mcp/public/all [get]
-func GetAllPublicMCPs(c *gin.Context) {
-	status, _ := strconv.Atoi(c.Query("status"))
-
-	if status == 0 {
-		status = int(model.PublicMCPStatusEnabled)
-	}
-
-	mcps, err := model.GetAllPublicMCPs(model.PublicMCPStatus(status))
-	if err != nil {
-		middleware.ErrorResponse(c, http.StatusInternalServerError, err.Error())
-		return
-	}
-	middleware.SuccessResponse(c, NewPublicMCPResponses(c.Request.Host, mcps))
-}
-
-// GetPublicMCPByIDHandler godoc
-//
-//	@Summary		Get MCP by ID
-//	@Description	Get a specific MCP by its ID
-//	@Tags			mcp
-//	@Produce		json
-//	@Security		ApiKeyAuth
-//	@Param			id	path		string	true	"MCP ID"
-//	@Success		200	{object}	middleware.APIResponse{data=PublicMCPResponse}
-//	@Router			/api/mcp/public/{id} [get]
-func GetPublicMCPByIDHandler(c *gin.Context) {
-	id := c.Param("id")
-	if id == "" {
-		middleware.ErrorResponse(c, http.StatusBadRequest, "MCP ID is required")
-		return
-	}
-
-	mcp, err := model.GetPublicMCPByID(id)
-	if err != nil {
-		middleware.ErrorResponse(c, http.StatusNotFound, err.Error())
-		return
-	}
-
-	middleware.SuccessResponse(c, NewPublicMCPResponse(c.Request.Host, mcp))
-}
-
-// CreatePublicMCP godoc
-//
-//	@Summary		Create MCP
-//	@Description	Create a new MCP
-//	@Tags			mcp
-//	@Accept			json
-//	@Produce		json
-//	@Security		ApiKeyAuth
-//	@Param			mcp	body		model.PublicMCP	true	"MCP object"
-//	@Success		200	{object}	middleware.APIResponse{data=PublicMCPResponse}
-//	@Router			/api/mcp/public/ [post]
-func CreatePublicMCP(c *gin.Context) {
-	var mcp model.PublicMCP
-	if err := c.ShouldBindJSON(&mcp); err != nil {
-		middleware.ErrorResponse(c, http.StatusBadRequest, err.Error())
-		return
-	}
-
-	if err := model.CreatePublicMCP(&mcp); err != nil {
-		middleware.ErrorResponse(c, http.StatusInternalServerError, err.Error())
-		return
-	}
-
-	middleware.SuccessResponse(c, NewPublicMCPResponse(c.Request.Host, mcp))
-}
-
-type UpdatePublicMCPStatusRequest struct {
-	Status model.PublicMCPStatus `json:"status"`
-}
-
-// UpdatePublicMCPStatus godoc
-//
-//	@Summary		Update MCP status
-//	@Description	Update the status of an MCP
-//	@Tags			mcp
-//	@Accept			json
-//	@Produce		json
-//	@Security		ApiKeyAuth
-//	@Param			id		path		string							true	"MCP ID"
-//	@Param			status	body		UpdatePublicMCPStatusRequest	true	"MCP status"
-//	@Success		200		{object}	middleware.APIResponse
-//	@Router			/api/mcp/public/{id}/status [post]
-func UpdatePublicMCPStatus(c *gin.Context) {
-	id := c.Param("id")
-	if id == "" {
-		middleware.ErrorResponse(c, http.StatusBadRequest, "MCP ID is required")
-		return
-	}
-
-	var status UpdatePublicMCPStatusRequest
-	if err := c.ShouldBindJSON(&status); err != nil {
-		middleware.ErrorResponse(c, http.StatusBadRequest, err.Error())
-		return
-	}
-
-	if err := model.UpdatePublicMCPStatus(id, status.Status); err != nil {
-		middleware.ErrorResponse(c, http.StatusInternalServerError, err.Error())
-		return
-	}
-
-	middleware.SuccessResponse(c, nil)
-}
-
-// UpdatePublicMCP godoc
-//
-//	@Summary		Update MCP
-//	@Description	Update an existing MCP
-//	@Tags			mcp
-//	@Accept			json
-//	@Produce		json
-//	@Security		ApiKeyAuth
-//	@Param			id	path		string			true	"MCP ID"
-//	@Param			mcp	body		model.PublicMCP	true	"MCP object"
-//	@Success		200	{object}	middleware.APIResponse{data=PublicMCPResponse}
-//	@Router			/api/mcp/public/{id} [put]
-func UpdatePublicMCP(c *gin.Context) {
-	id := c.Param("id")
-	if id == "" {
-		middleware.ErrorResponse(c, http.StatusBadRequest, "MCP ID is required")
-		return
-	}
-
-	var mcp model.PublicMCP
-	if err := c.ShouldBindJSON(&mcp); err != nil {
-		middleware.ErrorResponse(c, http.StatusBadRequest, err.Error())
-		return
-	}
-
-	mcp.ID = id
-
-	if err := model.UpdatePublicMCP(&mcp); err != nil {
-		middleware.ErrorResponse(c, http.StatusInternalServerError, err.Error())
-		return
-	}
-
-	middleware.SuccessResponse(c, NewPublicMCPResponse(c.Request.Host, mcp))
-}
-
-// DeletePublicMCP godoc
-//
-//	@Summary		Delete MCP
-//	@Description	Delete an MCP by ID
-//	@Tags			mcp
-//	@Produce		json
-//	@Security		ApiKeyAuth
-//	@Param			id	path		string	true	"MCP ID"
-//	@Success		200	{object}	middleware.APIResponse
-//	@Router			/api/mcp/public/{id} [delete]
-func DeletePublicMCP(c *gin.Context) {
-	id := c.Param("id")
-	if id == "" {
-		middleware.ErrorResponse(c, http.StatusBadRequest, "MCP ID is required")
-		return
-	}
-
-	if err := model.DeletePublicMCP(id); err != nil {
-		middleware.ErrorResponse(c, http.StatusInternalServerError, err.Error())
-		return
-	}
-
-	middleware.SuccessResponse(c, nil)
-}
-
-// GetGroupPublicMCPReusingParam godoc
-//
-//	@Summary		Get group MCP reusing parameters
-//	@Description	Get reusing parameters for a specific group and MCP
-//	@Tags			mcp
-//	@Produce		json
-//	@Security		ApiKeyAuth
-//	@Param			id		path		string	true	"MCP ID"
-//	@Param			group	path		string	true	"Group ID"
-//	@Success		200		{object}	middleware.APIResponse{data=model.PublicMCPReusingParam}
-//	@Router			/api/mcp/public/{id}/group/{group}/params [get]
-func GetGroupPublicMCPReusingParam(c *gin.Context) {
-	mcpID := c.Param("id")
-	groupID := c.Param("group")
-
-	if mcpID == "" || groupID == "" {
-		middleware.ErrorResponse(c, http.StatusBadRequest, "MCP ID and Group ID are required")
-		return
-	}
-
-	param, err := model.GetPublicMCPReusingParam(mcpID, groupID)
-	if err != nil {
-		middleware.ErrorResponse(c, http.StatusNotFound, err.Error())
-		return
-	}
-
-	middleware.SuccessResponse(c, param)
-}
-
-// SaveGroupPublicMCPReusingParam godoc
-//
-//	@Summary		Create or update group MCP reusing parameters
-//	@Description	Create or update reusing parameters for a specific group and MCP
-//	@Tags			mcp
-//	@Accept			json
-//	@Produce		json
-//	@Security		ApiKeyAuth
-//	@Param			id		path		string						true	"MCP ID"
-//	@Param			group	path		string						true	"Group ID"
-//	@Param			params	body		model.PublicMCPReusingParam	true	"Reusing parameters"
-//	@Success		200		{object}	middleware.APIResponse
-//	@Router			/api/mcp/public/{id}/group/{group}/params [post]
-func SaveGroupPublicMCPReusingParam(c *gin.Context) {
-	mcpID := c.Param("id")
-	groupID := c.Param("group")
-
-	if mcpID == "" || groupID == "" {
-		middleware.ErrorResponse(c, http.StatusBadRequest, "MCP ID and Group ID are required")
-		return
-	}
-
-	var param model.PublicMCPReusingParam
-	if err := c.ShouldBindJSON(&param); err != nil {
-		middleware.ErrorResponse(c, http.StatusBadRequest, err.Error())
-		return
-	}
-
-	param.MCPID = mcpID
-	param.GroupID = groupID
-
-	if err := model.SavePublicMCPReusingParam(&param); err != nil {
-		middleware.ErrorResponse(c, http.StatusInternalServerError, err.Error())
-		return
-	}
-
-	middleware.SuccessResponse(c, param)
-}
-
-```
-
-File: mcp-servers/server.go
-```go
-package mcpservers
-
-import (
-	"context"
-	"encoding/json"
-	"runtime"
-
-	"github.com/bytedance/sonic"
-	"github.com/mark3labs/mcp-go/client/transport"
-	"github.com/mark3labs/mcp-go/mcp"
-)
-
-type Server interface {
-	HandleMessage(ctx context.Context, message json.RawMessage) mcp.JSONRPCMessage
-}
-
-type client2Server struct {
-	client transport.Interface
-}
-
-func (s *client2Server) HandleMessage(
-	ctx context.Context,
-	message json.RawMessage,
-) mcp.JSONRPCMessage {
-	methodNode, err := sonic.Get(message, "method")
-	if err != nil {
-		return CreateMCPErrorResponse(nil, mcp.PARSE_ERROR, err.Error())
-	}
-	method, err := methodNode.String()
-	if err != nil {
-		return CreateMCPErrorResponse(nil, mcp.PARSE_ERROR, err.Error())
-	}
-
-	switch method {
-	case "notifications/initialized":
-		req := mcp.JSONRPCNotification{}
-		err := sonic.Unmarshal(message, &req)
-		if err != nil {
-			return CreateMCPErrorResponse(nil, mcp.PARSE_ERROR, err.Error())
-		}
-		err = s.client.SendNotification(ctx, req)
-		if err != nil {
-			return CreateMCPErrorResponse(nil, mcp.PARSE_ERROR, err.Error())
-		}
-		return nil
-	default:
-		req := transport.JSONRPCRequest{}
-		err := sonic.Unmarshal(message, &req)
-		if err != nil {
-			return CreateMCPErrorResponse(nil, mcp.PARSE_ERROR, err.Error())
-		}
-		resp, err := s.client.SendRequest(ctx, req)
-		if err != nil {
-			return CreateMCPErrorResponse(nil, mcp.INTERNAL_ERROR, err.Error())
-		}
-		if resp.Error != nil {
-			return CreateMCPErrorResponse(
-				resp.ID,
-				resp.Error.Code,
-				resp.Error.Message,
-				resp.Error.Data,
-			)
-		}
-		return CreateMCPResultResponse(
-			resp.ID,
-			resp.Result,
-		)
-	}
-}
-
-func WrapMCPClient2Server(client transport.Interface) Server {
-	return &client2Server{client: client}
-}
-
-func WrapMCPClient2ServerWithCleanup(client transport.Interface) Server {
-	server := &client2Server{client: client}
-	_ = runtime.AddCleanup(server, func(client transport.Interface) {
-		_ = client.Close()
-	}, server.client)
-	return server
-}
-
-type JSONRPCNoErrorResponse struct {
-	JSONRPC string          `json:"jsonrpc"`
-	ID      mcp.RequestId   `json:"id"`
-	Result  json.RawMessage `json:"result"`
-}
-
-func CreateMCPResultResponse(
-	id any,
-	result json.RawMessage,
-) mcp.JSONRPCMessage {
-	return &JSONRPCNoErrorResponse{
-		JSONRPC: mcp.JSONRPC_VERSION,
-		ID:      mcp.NewRequestId(id),
-		Result:  result,
-	}
-}
-
-func CreateMCPErrorResponse(
-	id any,
-	code int,
-	message string,
-	data ...any,
-) mcp.JSONRPCMessage {
-	var d any
-	if len(data) > 0 {
-		d = data[0]
-	}
-	return mcp.JSONRPCError{
-		JSONRPC: mcp.JSONRPC_VERSION,
-		ID:      mcp.NewRequestId(id),
-		Error: struct {
-			Code    int    `json:"code"`
-			Message string `json:"message"`
-			Data    any    `json:"data,omitempty"`
-		}{
-			Code:    code,
-			Message: message,
-			Data:    d,
-		},
-	}
-}
-
-```
-
-File: core/controller/mcp/embedmcp.go
-```go
-package controller
-
-import (
-	"context"
-	"fmt"
-	"maps"
-	"net/http"
-	"net/url"
-	"slices"
-	"strings"
-
-	"github.com/gin-gonic/gin"
-	"github.com/labring/aiproxy/core/mcpproxy"
-	"github.com/labring/aiproxy/core/middleware"
-	"github.com/labring/aiproxy/core/model"
-	mcpservers "github.com/labring/aiproxy/mcp-servers"
-	// init embed mcp
-	_ "github.com/labring/aiproxy/mcp-servers/mcpregister"
-	"github.com/mark3labs/mcp-go/mcp"
-)
-
-type EmbedMCPConfigTemplate struct {
-	Name        string `json:"name"`
-	Required    bool   `json:"required"`
-	Example     string `json:"example,omitempty"`
-	Description string `json:"description,omitempty"`
-}
-
-func newEmbedMCPConfigTemplate(template mcpservers.ConfigTemplate) EmbedMCPConfigTemplate {
-	return EmbedMCPConfigTemplate{
-		Name:        template.Name,
-		Required:    template.Required == mcpservers.ConfigRequiredTypeInitOnly,
-		Example:     template.Example,
-		Description: template.Description,
-	}
-}
-
-type EmbedMCPConfigTemplates = map[string]EmbedMCPConfigTemplate
-
-func newEmbedMCPConfigTemplates(templates mcpservers.ConfigTemplates) EmbedMCPConfigTemplates {
-	emcpTemplates := make(EmbedMCPConfigTemplates, len(templates))
-	for key, template := range templates {
-		emcpTemplates[key] = newEmbedMCPConfigTemplate(template)
-	}
-	return emcpTemplates
-}
-
-type EmbedMCP struct {
-	ID              string                  `json:"id"`
-	Enabled         bool                    `json:"enabled"`
-	Name            string                  `json:"name"`
-	Readme          string                  `json:"readme"`
-	Tags            []string                `json:"tags"`
-	ConfigTemplates EmbedMCPConfigTemplates `json:"config_templates"`
-}
-
-func newEmbedMCP(mcp *mcpservers.McpServer, enabled bool) *EmbedMCP {
-	emcp := &EmbedMCP{
-		ID:              mcp.ID,
-		Enabled:         enabled,
-		Name:            mcp.Name,
-		Readme:          mcp.Readme,
-		Tags:            mcp.Tags,
-		ConfigTemplates: newEmbedMCPConfigTemplates(mcp.ConfigTemplates),
-	}
-	return emcp
-}
-
-// GetEmbedMCPs godoc
-//
-//	@Summary		Get embed mcp
-//	@Description	Get embed mcp
-//	@Tags			embedmcp
-//	@Accept			json
-//	@Produce		json
-//	@Security		ApiKeyAuth
-//	@Success		200	{array}	EmbedMCP
-//	@Router			/api/embedmcp/ [get]
-func GetEmbedMCPs(c *gin.Context) {
-	embeds := mcpservers.Servers()
-	enabledMCPs, err := model.GetPublicMCPsEnabled(slices.Collect(maps.Keys(embeds)))
-	if err != nil {
-		middleware.ErrorResponse(c, http.StatusInternalServerError, err.Error())
-		return
-	}
-
-	emcps := make([]*EmbedMCP, 0, len(embeds))
-	for _, mcp := range embeds {
-		emcps = append(emcps, newEmbedMCP(&mcp, slices.Contains(enabledMCPs, mcp.ID)))
-	}
-
-	middleware.SuccessResponse(c, emcps)
-}
-
-type SaveEmbedMCPRequest struct {
-	ID         string            `json:"id"`
-	Enabled    bool              `json:"enabled"`
-	InitConfig map[string]string `json:"init_config"`
-}
-
-func GetEmbedConfig(
-	ct mcpservers.ConfigTemplates,
-	initConfig map[string]string,
-) (*model.MCPEmbeddingConfig, error) {
-	reusingConfig := make(map[string]model.MCPEmbeddingReusingConfig)
-	embedConfig := &model.MCPEmbeddingConfig{
-		Init: initConfig,
-	}
-	for key, value := range ct {
-		switch value.Required {
-		case mcpservers.ConfigRequiredTypeInitOnly:
-			if v, ok := initConfig[key]; !ok || v == "" {
-				return nil, fmt.Errorf("config %s is required", key)
-			}
-		case mcpservers.ConfigRequiredTypeReusingOnly:
-			if _, ok := initConfig[key]; ok {
-				return nil, fmt.Errorf("config %s is provided, but it is not allowed", key)
-			}
-			reusingConfig[key] = model.MCPEmbeddingReusingConfig{
-				Name:        value.Name,
-				Description: value.Description,
-				Required:    true,
-			}
-		case mcpservers.ConfigRequiredTypeInitOrReusingOnly:
-			if v, ok := initConfig[key]; ok {
-				if v == "" {
-					return nil, fmt.Errorf("config %s is required", key)
-				}
-				continue
-			}
-			reusingConfig[key] = model.MCPEmbeddingReusingConfig{
-				Name:        value.Name,
-				Description: value.Description,
-				Required:    true,
-			}
-		}
-	}
-	embedConfig.Reusing = reusingConfig
-	return embedConfig, nil
-}
-
-func ToPublicMCP(
-	e mcpservers.McpServer,
-	initConfig map[string]string,
-	enabled bool,
-) (*model.PublicMCP, error) {
-	embedConfig, err := GetEmbedConfig(e.ConfigTemplates, initConfig)
-	if err != nil {
-		return nil, err
-	}
-	pmcp := &model.PublicMCP{
-		ID:          e.ID,
-		Name:        e.Name,
-		LogoURL:     e.LogoURL,
-		Readme:      e.Readme,
-		Tags:        e.Tags,
-		EmbedConfig: embedConfig,
-	}
-	if enabled {
-		pmcp.Status = model.PublicMCPStatusEnabled
-	} else {
-		pmcp.Status = model.PublicMCPStatusDisabled
-	}
-	switch e.Type {
-	case mcpservers.McpTypeEmbed:
-		pmcp.Type = model.PublicMCPTypeEmbed
-	case mcpservers.McpTypeDocs:
-		pmcp.Type = model.PublicMCPTypeDocs
-	}
-	return pmcp, nil
-}
-
-// SaveEmbedMCP godoc
-//
-//	@Summary		Save embed mcp
-//	@Description	Save embed mcp
-//	@Tags			embedmcp
-//	@Accept			json
-//	@Produce		json
-//	@Security		ApiKeyAuth
-//	@Param			body	body		SaveEmbedMCPRequest	true	"Save embed mcp request"
-//	@Success		200		{object}	nil
-//	@Router			/api/embedmcp/ [post]
-func SaveEmbedMCP(c *gin.Context) {
-	var req SaveEmbedMCPRequest
-	if err := c.ShouldBindJSON(&req); err != nil {
-		middleware.ErrorResponse(c, http.StatusBadRequest, err.Error())
-		return
-	}
-
-	emcp, ok := mcpservers.GetEmbedMCP(req.ID)
-	if !ok {
-		middleware.ErrorResponse(c, http.StatusNotFound, "embed mcp not found")
-		return
-	}
-
-	pmcp, err := ToPublicMCP(emcp, req.InitConfig, req.Enabled)
-	if err != nil {
-		middleware.ErrorResponse(c, http.StatusInternalServerError, err.Error())
-		return
-	}
-
-	if err := model.SavePublicMCP(pmcp); err != nil {
-		middleware.ErrorResponse(c, http.StatusInternalServerError, err.Error())
-		return
-	}
-
-	middleware.SuccessResponse(c, nil)
-}
-
-type testEmbedMcpEndpointProvider struct {
-	key string
-}
-
-func newTestEmbedMcpEndpoint(key string) EndpointProvider {
-	return &testEmbedMcpEndpointProvider{
-		key: key,
-	}
-}
-
-func (m *testEmbedMcpEndpointProvider) NewEndpoint(session string) (newEndpoint string) {
-	endpoint := fmt.Sprintf("/api/test-embedmcp/message?sessionId=%s&key=%s", session, m.key)
-	return endpoint
-}
-
-func (m *testEmbedMcpEndpointProvider) LoadEndpoint(endpoint string) (session string) {
-	parsedURL, err := url.Parse(endpoint)
-	if err != nil {
-		return ""
-	}
-	return parsedURL.Query().Get("sessionId")
-}
-
-// query like:
-// /api/test-embedmcp/aiproxy-openapi/sse?key=adminkey&config[key1]=value1&config[key2]=value2&reusing[key3]=value3
-func getConfigFromQuery(c *gin.Context) (map[string]string, map[string]string) {
-	initConfig := make(map[string]string)
-	reusingConfig := make(map[string]string)
-
-	queryParams := c.Request.URL.Query()
-
-	for paramName, paramValues := range queryParams {
-		if len(paramValues) == 0 {
-			continue
-		}
-
-		paramValue := paramValues[0]
-
-		if strings.HasPrefix(paramName, "config[") && strings.HasSuffix(paramName, "]") {
-			key := paramName[7 : len(paramName)-1]
-			if key != "" {
-				initConfig[key] = paramValue
-			}
-		}
-
-		if strings.HasPrefix(paramName, "reusing[") && strings.HasSuffix(paramName, "]") {
-			key := paramName[8 : len(paramName)-1]
-			if key != "" {
-				reusingConfig[key] = paramValue
-			}
-		}
-	}
-
-	return initConfig, reusingConfig
-}
-
-// TestEmbedMCPSseServer godoc
-//
-//	@Summary		Test Embed MCP SSE Server
-//	@Description	Test Embed MCP SSE Server
-//	@Tags			embedmcp
-//	@Security		ApiKeyAuth
-//	@Param			id				path		string	true	"MCP ID"
-//	@Param			config[key]		query		string	false	"Initial configuration parameters (e.g. config[host]=http://localhost:3000)"
-//	@Param			reusing[key]	query		string	false	"Reusing configuration parameters (e.g. reusing[authorization]=apikey)"
-//	@Success		200				{object}	nil
-//	@Failure		400				{object}	nil
-//	@Router			/api/test-embedmcp/{id}/sse [get]
-func TestEmbedMCPSseServer(c *gin.Context) {
-	id := c.Param("id")
-	if id == "" {
-		http.Error(c.Writer, "mcp id is required", http.StatusBadRequest)
-		return
-	}
-
-	initConfig, reusingConfig := getConfigFromQuery(c)
-	emcp, err := mcpservers.GetMCPServer(id, initConfig, reusingConfig)
-	if err != nil {
-		http.Error(c.Writer, err.Error(), http.StatusBadRequest)
-		return
-	}
-
-	handleTestEmbedMCPServer(c, emcp)
-}
-
-const (
-	testEmbedMcpType = "test-embedmcp"
-)
-
-func handleTestEmbedMCPServer(c *gin.Context, s mcpservers.Server) {
-	token := middleware.GetToken(c)
-
-	// Store the session
-	store := getStore()
-	newSession := store.New()
-
-	newEndpoint := newTestEmbedMcpEndpoint(token.Key).NewEndpoint(newSession)
-	server := mcpproxy.NewSSEServer(
-		s,
-		mcpproxy.WithMessageEndpoint(newEndpoint),
-	)
-
-	store.Set(newSession, testEmbedMcpType)
-	defer func() {
-		store.Delete(newSession)
-	}()
-
-	ctx, cancel := context.WithCancel(c.Request.Context())
-	defer cancel()
-
-	// Start message processing goroutine
-	go processMCPSSEMpscMessages(ctx, newSession, server)
-
-	// Handle SSE connection
-	server.ServeHTTP(c.Writer, c.Request)
-}
-
-// TestEmbedMCPMessage godoc
-//
-//	@Summary		Test Embed MCP Message
-//	@Description	Send a message to the test embed MCP server
-//	@Tags			embedmcp
-//	@Security		ApiKeyAuth
-//	@Param			sessionId	query	string	true	"Session ID"
-//	@Accept			json
-//	@Produce		json
-//	@Success		200	{object}	nil
-//	@Failure		400	{object}	nil
-//	@Router			/api/test-embedmcp/message [post]
-func TestEmbedMCPMessage(c *gin.Context) {
-	sessionID, _ := c.GetQuery("sessionId")
-	if sessionID == "" {
-		http.Error(c.Writer, "missing sessionId", http.StatusBadRequest)
-		return
-	}
-
-	sendMCPSSEMessage(c, testEmbedMcpType, sessionID)
-}
-
-// TestEmbedMCPStreamable godoc
-//
-//	@Summary		Test Embed MCP Streamable Server
-//	@Description	Test Embed MCP Streamable Server with various HTTP methods
-//	@Tags			embedmcp
-//	@Security		ApiKeyAuth
-//	@Param			id				path	string	true	"MCP ID"
-//	@Param			config[key]		query	string	false	"Initial configuration parameters (e.g. config[host]=http://localhost:3000)"
-//	@Param			reusing[key]	query	string	false	"Reusing configuration parameters (e.g., reusing[authorization]=apikey)"
-//	@Accept			json
-//	@Produce		json
-//	@Success		200	{object}	nil
-//	@Failure		400	{object}	nil
-//	@Router			/api/test-embedmcp/{id} [get]
-//	@Router			/api/test-embedmcp/{id} [post]
-//	@Router			/api/test-embedmcp/{id} [delete]
-func TestEmbedMCPStreamable(c *gin.Context) {
-	id := c.Param("id")
-	if id == "" {
-		c.JSON(http.StatusBadRequest, mcpservers.CreateMCPErrorResponse(
-			mcp.NewRequestId(nil),
-			mcp.INVALID_REQUEST,
-			"mcp id is required",
-		))
-		return
-	}
-
-	initConfig, reusingConfig := getConfigFromQuery(c)
-	server, err := mcpservers.GetMCPServer(id, initConfig, reusingConfig)
-	if err != nil {
-		c.JSON(http.StatusBadRequest, mcpservers.CreateMCPErrorResponse(
-			mcp.NewRequestId(nil),
-			mcp.INVALID_REQUEST,
-			err.Error(),
-		))
-		return
-	}
-	handleStreamableMCPServer(c, server)
-}
-
-```
-
-File: core/model/publicmcp.go
-```go
-package model
-
-import (
-	"errors"
-	"net/url"
-	"regexp"
-	"time"
-
-	"github.com/bytedance/sonic"
-	"github.com/labring/aiproxy/core/common"
-	log "github.com/sirupsen/logrus"
-	"gorm.io/gorm"
-)
-
-type PublicMCPStatus int
-
-const (
-	PublicMCPStatusEnabled PublicMCPStatus = iota + 1
-	PublicMCPStatusDisabled
-)
-
-const (
-	ErrPublicMCPNotFound       = "public mcp"
-	ErrMCPReusingParamNotFound = "mcp reusing param"
-)
-
-type PublicMCPType string
-
-const (
-	PublicMCPTypeProxySSE        PublicMCPType = "mcp_proxy_sse"
-	PublicMCPTypeProxyStreamable PublicMCPType = "mcp_proxy_streamable"
-	PublicMCPTypeDocs            PublicMCPType = "mcp_docs" // read only
-	PublicMCPTypeOpenAPI         PublicMCPType = "mcp_openapi"
-	PublicMCPTypeEmbed           PublicMCPType = "mcp_embed"
-)
-
-type ParamType string
-
-const (
-	ParamTypeHeader ParamType = "header"
-	ParamTypeQuery  ParamType = "query"
-)
-
-type ReusingParam struct {
-	Name        string    `json:"name"`
-	Description string    `json:"description"`
-	Type        ParamType `json:"type"`
-	Required    bool      `json:"required"`
-}
-
-type MCPPrice struct {
-	DefaultToolsCallPrice float64            `json:"default_tools_call_price"`
-	ToolsCallPrices       map[string]float64 `json:"tools_call_prices"        gorm:"serializer:fastjson;type:text"`
-}
-
-type PublicMCPProxyConfig struct {
-	URL           string                  `json:"url"`
-	Querys        map[string]string       `json:"querys"`
-	Headers       map[string]string       `json:"headers"`
-	ReusingParams map[string]ReusingParam `json:"reusing_params"`
-}
-
-type PublicMCPReusingParam struct {
-	MCPID         string            `gorm:"primaryKey"                    json:"mcp_id"`
-	GroupID       string            `gorm:"primaryKey"                    json:"group_id"`
-	CreatedAt     time.Time         `gorm:"index"                         json:"created_at"`
-	UpdateAt      time.Time         `gorm:"index"                         json:"update_at"`
-	Group         *Group            `gorm:"foreignKey:GroupID"            json:"-"`
-	ReusingParams map[string]string `gorm:"serializer:fastjson;type:text" json:"reusing_params"`
-}
-
-func (p *PublicMCPReusingParam) BeforeCreate(_ *gorm.DB) (err error) {
-	if p.MCPID == "" {
-		return errors.New("mcp id is empty")
-	}
-	if p.GroupID == "" {
-		return errors.New("group is empty")
-	}
-	return
-}
-
-func (p *PublicMCPReusingParam) MarshalJSON() ([]byte, error) {
-	type Alias PublicMCPReusingParam
-	a := &struct {
-		*Alias
-		CreatedAt int64 `json:"created_at"`
-		UpdateAt  int64 `json:"update_at"`
-	}{
-		Alias:     (*Alias)(p),
-		CreatedAt: p.CreatedAt.UnixMilli(),
-		UpdateAt:  p.UpdateAt.UnixMilli(),
-	}
-	return sonic.Marshal(a)
-}
-
-type MCPOpenAPIConfig struct {
-	OpenAPISpec    string `json:"openapi_spec"`
-	OpenAPIContent string `json:"openapi_content,omitempty"`
-	V2             bool   `json:"v2"`
-	ServerAddr     string `json:"server_addr,omitempty"`
-	Authorization  string `json:"authorization,omitempty"`
-}
-
-type MCPEmbeddingReusingConfig struct {
-	Name        string `json:"name"`
-	Description string `json:"description"`
-	Required    bool   `json:"required"`
-}
-
-type MCPEmbeddingConfig struct {
-	Init    map[string]string                    `json:"init"`
-	Reusing map[string]MCPEmbeddingReusingConfig `json:"reusing"`
-}
-
-var validateMCPIDRegex = regexp.MustCompile(`^[a-zA-Z0-9_-]+)
-
-func validateMCPID(id string) error {
-	if id == "" {
-		return errors.New("mcp id is empty")
-	}
-	if !validateMCPIDRegex.MatchString(id) {
-		return errors.New("mcp id is invalid")
-	}
-	return nil
-}
-
-type PublicMCP struct {
-	ID                     string                  `gorm:"primaryKey"                    json:"id"`
-	Status                 PublicMCPStatus         `gorm:"index;default:1"               json:"status"`
-	CreatedAt              time.Time               `gorm:"index,autoCreateTime"          json:"created_at"`
-	UpdateAt               time.Time               `gorm:"index,autoUpdateTime"          json:"update_at"`
-	PublicMCPReusingParams []PublicMCPReusingParam `gorm:"foreignKey:MCPID"              json:"-"`
-	Name                   string                  `                                     json:"name"`
-	Type                   PublicMCPType           `gorm:"index"                         json:"type"`
-	RepoURL                string                  `                                     json:"repo_url"`
-	ReadmeURL              string                  `                                     json:"readme_url"`
-	Readme                 string                  `gorm:"type:text"                     json:"readme"`
-	Tags                   []string                `gorm:"serializer:fastjson;type:text" json:"tags,omitempty"`
-	LogoURL                string                  `                                     json:"logo_url"`
-	Price                  MCPPrice                `gorm:"embedded"                      json:"price"`
-	ProxyConfig            *PublicMCPProxyConfig   `gorm:"serializer:fastjson;type:text" json:"proxy_config,omitempty"`
-	OpenAPIConfig          *MCPOpenAPIConfig       `gorm:"serializer:fastjson;type:text" json:"openapi_config,omitempty"`
-	EmbedConfig            *MCPEmbeddingConfig     `gorm:"serializer:fastjson;type:text" json:"embed_config,omitempty"`
-}
-
-func (p *PublicMCP) BeforeSave(_ *gorm.DB) error {
-	if err := validateMCPID(p.ID); err != nil {
-		return err
-	}
-
-	if p.Status == 0 {
-		p.Status = PublicMCPStatusEnabled
-	}
-
-	if p.OpenAPIConfig != nil {
-		config := p.OpenAPIConfig
-		if config.OpenAPISpec != "" {
-			return validateHTTPURL(config.OpenAPISpec)
-		}
-		if config.OpenAPIContent != "" {
-			return nil
-		}
-		return errors.New("openapi spec and content is empty")
-	}
-
-	if p.ProxyConfig != nil {
-		config := p.ProxyConfig
-		return validateHTTPURL(config.URL)
-	}
-	return nil
-}
-
-func validateHTTPURL(str string) error {
-	if str == "" {
-		return errors.New("url is empty")
-	}
-	u, err := url.Parse(str)
-	if err != nil {
-		return err
-	}
-	if u.Scheme != "http" && u.Scheme != "https" {
-		return errors.New("url scheme not support")
-	}
-	return nil
-}
-
-func (p *PublicMCP) BeforeDelete(tx *gorm.DB) (err error) {
-	return tx.Model(&PublicMCPReusingParam{}).
-		Where("mcp_id = ?", p.ID).
-		Delete(&PublicMCPReusingParam{}).
-		Error
-}
-
-// CreatePublicMCP creates a new MCP
-func CreatePublicMCP(mcp *PublicMCP) error {
-	err := DB.Create(mcp).Error
-	if err != nil && errors.Is(err, gorm.ErrDuplicatedKey) {
-		return errors.New("mcp server already exist")
-	}
-	return err
-}
-
-func SavePublicMCP(mcp *PublicMCP) (err error) {
-	defer func() {
-		if err == nil {
-			if err := CacheDeletePublicMCP(mcp.ID); err != nil {
-				log.Error("cache delete public mcp error: " + err.Error())
-			}
-		}
-	}()
-
-	return DB.Save(mcp).Error
-}
-
-// UpdatePublicMCP updates an existing MCP
-func UpdatePublicMCP(mcp *PublicMCP) (err error) {
-	defer func() {
-		if err == nil {
-			if err := CacheDeletePublicMCP(mcp.ID); err != nil {
-				log.Error("cache delete public mcp error: " + err.Error())
-			}
-		}
-	}()
-
-	selects := []string{
-		"repo_url",
-		"readme",
-		"readme_url",
-		"tags",
-		"author",
-		"logo_url",
-		"proxy_config",
-		"openapi_config",
-		"embed_config",
-	}
-	if mcp.Status != 0 {
-		selects = append(selects, "status")
-	}
-	if mcp.Name != "" {
-		selects = append(selects, "name")
-	}
-	if mcp.Type != "" {
-		selects = append(selects, "type")
-	}
-	if mcp.Price.DefaultToolsCallPrice != 0 ||
-		len(mcp.Price.ToolsCallPrices) != 0 {
-		selects = append(selects, "price")
-	}
-	result := DB.
-		Select(selects).
-		Where("id = ?", mcp.ID).
-		Updates(mcp)
-	return HandleUpdateResult(result, ErrPublicMCPNotFound)
-}
-
-func UpdatePublicMCPStatus(id string, status PublicMCPStatus) (err error) {
-	defer func() {
-		if err == nil {
-			if err := CacheUpdatePublicMCPStatus(id, status); err != nil {
-				log.Error("cache update public mcp status error: " + err.Error())
-			}
-		}
-	}()
-
-	result := DB.Model(&PublicMCP{}).Where("id = ?", id).Update("status", status)
-	return HandleUpdateResult(result, ErrPublicMCPNotFound)
-}
-
-// DeletePublicMCP deletes an MCP by ID
-func DeletePublicMCP(id string) (err error) {
-	defer func() {
-		if err == nil {
-			if err := CacheDeletePublicMCP(id); err != nil {
-				log.Error("cache delete public mcp error: " + err.Error())
-			}
-		}
-	}()
-
-	if id == "" {
-		return errors.New("MCP id is empty")
-	}
-	result := DB.Delete(&PublicMCP{ID: id})
-	return HandleUpdateResult(result, ErrPublicMCPNotFound)
-}
-
-// GetPublicMCPByID retrieves an MCP by ID
-func GetPublicMCPByID(id string) (PublicMCP, error) {
-	var mcp PublicMCP
-	if id == "" {
-		return mcp, errors.New("MCP id is empty")
-	}
-	err := DB.Where("id = ?", id).First(&mcp).Error
-	return mcp, HandleNotFound(err, ErrPublicMCPNotFound)
-}
-
-// GetPublicMCPs retrieves MCPs with pagination and filtering
-func GetPublicMCPs(
-	page, perPage int,
-	mcpType PublicMCPType,
-	keyword string,
-	status PublicMCPStatus,
-) (mcps []PublicMCP, total int64, err error) {
-	tx := DB.Model(&PublicMCP{})
-
-	if mcpType != "" {
-		tx = tx.Where("type = ?", mcpType)
-	}
-
-	if keyword != "" {
-		keyword = "%" + keyword + "%"
-		if common.UsingPostgreSQL {
-			tx = tx.Where(
-				"name ILIKE ? OR author ILIKE ? OR tags ILIKE ? OR id ILIKE ?",
-				keyword,
-				keyword,
-				keyword,
-				keyword,
-			)
-		} else {
-			tx = tx.Where("name LIKE ? OR author LIKE ? OR tags LIKE ? OR id LIKE ?", keyword, keyword, keyword, keyword)
-		}
-	}
-
-	if status != 0 {
-		tx = tx.Where("status = ?", status)
-	}
-
-	err = tx.Count(&total).Error
-	if err != nil {
-		return nil, 0, err
-	}
-
-	if total <= 0 {
-		return nil, 0, nil
-	}
-
-	limit, offset := toLimitOffset(page, perPage)
-	err = tx.
-		Limit(limit).
-		Offset(offset).
-		Find(&mcps).
-		Error
-
-	return mcps, total, err
-}
-
-func GetAllPublicMCPs(status PublicMCPStatus) ([]PublicMCP, error) {
-	var mcps []PublicMCP
-	tx := DB.Model(&PublicMCP{})
-	if status != 0 {
-		tx = tx.Where("status = ?", status)
-	}
-	err := tx.Find(&mcps).Error
-	return mcps, err
-}
-
-func GetPublicMCPsEnabled(ids []string) ([]string, error) {
-	var mcpIDs []string
-	err := DB.Model(&PublicMCP{}).
-		Select("id").
-		Where("id IN (?) AND status = ?", ids, PublicMCPStatusEnabled).
-		Pluck("id", &mcpIDs).
-		Error
-	if err != nil {
-		return nil, err
-	}
-	return mcpIDs, nil
-}
-
-func SavePublicMCPReusingParam(param *PublicMCPReusingParam) (err error) {
-	defer func() {
-		if err == nil {
-			if err := CacheDeletePublicMCPReusingParam(param.MCPID, param.GroupID); err != nil {
-				log.Error("cache delete public mcp reusing param error: " + err.Error())
-			}
-		}
-	}()
-
-	return DB.Save(param).Error
-}
-
-// UpdatePublicMCPReusingParam updates an existing GroupMCPReusingParam
-func UpdatePublicMCPReusingParam(param *PublicMCPReusingParam) (err error) {
-	defer func() {
-		if err == nil {
-			if err := CacheDeletePublicMCPReusingParam(param.MCPID, param.GroupID); err != nil {
-				log.Error("cache delete public mcp reusing param error: " + err.Error())
-			}
-		}
-	}()
-
-	result := DB.
-		Select([]string{
-			"reusing_params",
-		}).
-		Where("mcp_id = ? AND group_id = ?", param.MCPID, param.GroupID).
-		Updates(param)
-	return HandleUpdateResult(result, ErrMCPReusingParamNotFound)
-}
-
-// DeletePublicMCPReusingParam deletes a GroupMCPReusingParam
-func DeletePublicMCPReusingParam(mcpID, groupID string) (err error) {
-	defer func() {
-		if err == nil {
-			if err := CacheDeletePublicMCPReusingParam(mcpID, groupID); err != nil {
-				log.Error("cache delete public mcp reusing param error: " + err.Error())
-			}
-		}
-	}()
-
-	if mcpID == "" || groupID == "" {
-		return errors.New("MCP ID or Group ID is empty")
-	}
-	result := DB.
-		Where("mcp_id = ? AND group_id = ?", mcpID, groupID).
-		Delete(&PublicMCPReusingParam{})
-	return HandleUpdateResult(result, ErrMCPReusingParamNotFound)
-}
-
-// GetPublicMCPReusingParam retrieves a GroupMCPReusingParam by MCP ID and Group ID
-func GetPublicMCPReusingParam(mcpID, groupID string) (*PublicMCPReusingParam, error) {
-	if mcpID == "" || groupID == "" {
-		return nil, errors.New("MCP ID or Group ID is empty")
-	}
-	var param PublicMCPReusingParam
-	err := DB.Where("mcp_id = ? AND group_id = ?", mcpID, groupID).First(&param).Error
-	return &param, HandleNotFound(err, ErrMCPReusingParamNotFound)
-}
-
-```
-
-File: mcp-servers/mcp.go
-```go
-package mcpservers
-
-import (
-	"errors"
-	"fmt"
-)
-
-type ConfigValueValidator func(value string) error
-
-type ConfigRequiredType int
-
-const (
-	ConfigRequiredTypeInitOptional ConfigRequiredType = iota
-	ConfigRequiredTypeReusingOptional
-	ConfigRequiredTypeInitOnly
-	ConfigRequiredTypeReusingOnly
-	ConfigRequiredTypeInitOrReusingOnly
-)
-
-func (c ConfigRequiredType) Validate(config, reusingConfig string) error {
-	switch c {
-	case ConfigRequiredTypeInitOnly:
-		if config == "" {
-			return errors.New("config is required")
-		}
-	case ConfigRequiredTypeReusingOnly:
-		if reusingConfig == "" {
-			return errors.New("reusing config is required")
-		}
-	case ConfigRequiredTypeInitOrReusingOnly:
-		if config == "" && reusingConfig == "" {
-			return errors.New("config or reusing config is required")
-		}
-		if config != "" && reusingConfig != "" {
-			return errors.New(
-				"config and reusing config are both provided, but only one is allowed",
-			)
-		}
-	}
-	return nil
-}
-
-type ConfigTemplate struct {
-	Name        string               `json:"name"`
-	Required    ConfigRequiredType   `json:"required"`
-	Example     string               `json:"example,omitempty"`
-	Description string               `json:"description,omitempty"`
-	Validator   ConfigValueValidator `json:"-"`
-}
-
-type ConfigTemplates = map[string]ConfigTemplate
-
-func ValidateConfigTemplatesConfig(
-	ct ConfigTemplates,
-	config, reusingConfig map[string]string,
-) error {
-	if len(ct) == 0 {
-		return nil
-	}
-
-	for key, template := range ct {
-		c := config[key]
-		rc := reusingConfig[key]
-		if err := template.Required.Validate(c, rc); err != nil {
-			return fmt.Errorf("config required %s is invalid: %w", key, err)
-		}
-		if template.Validator != nil {
-			if c != "" {
-				if err := template.Validator(c); err != nil {
-					return fmt.Errorf("config %s is invalid: %w", key, err)
-				}
-			} else if rc != "" {
-				if err := template.Validator(rc); err != nil {
-					return fmt.Errorf("reusing config %s is invalid: %w", key, err)
-				}
-			}
-		}
-	}
-
-	return nil
-}
-
-func CheckConfigTemplatesValidate(ct ConfigTemplates) error {
-	for key, value := range ct {
-		if value.Name == "" {
-			return fmt.Errorf("config %s name is required", key)
-		}
-		if value.Description == "" {
-			return fmt.Errorf("config %s description is required", key)
-		}
-		if value.Example == "" || value.Validator == nil {
-			continue
-		}
-		if err := value.Validator(value.Example); err != nil {
-			return fmt.Errorf("config %s example is invalid: %w", key, err)
-		}
-	}
-	return nil
-}
-
-type NewServerFunc func(config, reusingConfig map[string]string) (Server, error)
-
-type McpType string
-
-const (
-	McpTypeEmbed McpType = "embed"
-	McpTypeDocs  McpType = "docs"
-)
-
-type McpServer struct {
-	ID              string
-	Name            string
-	Type            McpType
-	Readme          string
-	LogoURL         string
-	Tags            []string
-	ConfigTemplates ConfigTemplates
-	newServer       NewServerFunc
-}
-
-type McpConfig func(*McpServer)
-
-func WithReadme(readme string) McpConfig {
-	return func(e *McpServer) {
-		e.Readme = readme
-	}
-}
-
-func WithType(t McpType) McpConfig {
-	return func(e *McpServer) {
-		e.Type = t
-	}
-}
-
-func WithLogoURL(logoURL string) McpConfig {
-	return func(e *McpServer) {
-		e.LogoURL = logoURL
-	}
-}
-
-func WithTags(tags []string) McpConfig {
-	return func(e *McpServer) {
-		e.Tags = tags
-	}
-}
-
-func WithConfigTemplates(configTemplates ConfigTemplates) McpConfig {
-	return func(e *McpServer) {
-		e.ConfigTemplates = configTemplates
-	}
-}
-
-func WithNewServerFunc(newServer NewServerFunc) McpConfig {
-	return func(e *McpServer) {
-		e.newServer = newServer
-	}
-}
-
-func NewMcp(id, name string, mcpType McpType, opts ...McpConfig) McpServer {
-	e := McpServer{
-		ID:   id,
-		Name: name,
-		Type: mcpType,
-	}
-	for _, opt := range opts {
-		opt(&e)
-	}
-	return e
-}
-
-func (e *McpServer) NewServer(config, reusingConfig map[string]string) (Server, error) {
-	if err := ValidateConfigTemplatesConfig(e.ConfigTemplates, config, reusingConfig); err != nil {
-		return nil, fmt.Errorf("mcp %s config is invalid: %w", e.ID, err)
-	}
-	return e.newServer(config, reusingConfig)
-}
-
-```
-
-File: mcp-servers/register.go
-```go
-package mcpservers
-
-import (
-	"fmt"
-	"sort"
-	"strings"
-	"sync"
-	"sync/atomic"
-	"time"
-)
-
-type mcpServerCacheItem struct {
-	MCPServer         Server
-	LastUsedTimestamp atomic.Int64
-}
-
-var (
-	servers             = make(map[string]McpServer)
-	mcpServerCache      = make(map[string]*mcpServerCacheItem)
-	mcpServerCacheLock  = sync.RWMutex{}
-	cacheExpirationTime = 3 * time.Minute
-)
-
-func startCacheCleaner(interval time.Duration) {
-	go func() {
-		ticker := time.NewTicker(interval)
-		defer ticker.Stop()
-
-		for range ticker.C {
-			cleanupExpiredCache()
-		}
-	}()
-}
-
-func cleanupExpiredCache() {
-	now := time.Now().Unix()
-	expiredTime := now - int64(cacheExpirationTime.Seconds())
-
-	mcpServerCacheLock.Lock()
-	defer mcpServerCacheLock.Unlock()
-
-	for key, item := range mcpServerCache {
-		if item.LastUsedTimestamp.Load() < expiredTime {
-			delete(mcpServerCache, key)
-		}
-	}
-}
-
-func init() {
-	startCacheCleaner(time.Minute)
-}
-
-func Register(mcp McpServer) {
-	if mcp.ID == "" {
-		panic("mcp id is required")
-	}
-	if mcp.Name == "" {
-		panic("mcp name is required")
-	}
-	switch mcp.Type {
-	case McpTypeEmbed:
-		if mcp.newServer == nil {
-			panic(fmt.Sprintf("mcp %s new server is required", mcp.ID))
-		}
-	case McpTypeDocs:
-		if mcp.Readme == "" {
-			panic(fmt.Sprintf("mcp %s readme is required", mcp.ID))
-		}
-	default:
-		panic(fmt.Sprintf("mcp %s type is invalid", mcp.ID))
-	}
-
-	if mcp.ConfigTemplates != nil {
-		if err := CheckConfigTemplatesValidate(mcp.ConfigTemplates); err != nil {
-			panic(fmt.Sprintf("mcp %s config templates example is invalid: %v", mcp.ID, err))
-		}
-	}
-	if _, ok := servers[mcp.ID]; ok {
-		panic(fmt.Sprintf("mcp %s already registered", mcp.ID))
-	}
-	servers[mcp.ID] = mcp
-}
-
-func GetMCPServer(id string, config, reusingConfig map[string]string) (Server, error) {
-	embedServer, ok := servers[id]
-	if !ok {
-		return nil, fmt.Errorf("mcp %s not found", id)
-	}
-	if len(embedServer.ConfigTemplates) == 0 {
-		return loadCacheServer(embedServer, nil)
-	}
-
-	if err := ValidateConfigTemplatesConfig(embedServer.ConfigTemplates, config, reusingConfig); err != nil {
-		return nil, fmt.Errorf("mcp %s config is invalid: %w", id, err)
-	}
-
-	for _, template := range embedServer.ConfigTemplates {
-		switch template.Required {
-		case ConfigRequiredTypeReusingOptional,
-			ConfigRequiredTypeReusingOnly,
-			ConfigRequiredTypeInitOrReusingOnly:
-			return embedServer.NewServer(config, reusingConfig)
-		}
-	}
-
-	return loadCacheServer(embedServer, config)
-}
-
-func buildNoReusingConfigCacheKey(config map[string]string) string {
-	keys := make([]string, 0, len(config))
-	for key, value := range config {
-		keys = append(keys, fmt.Sprintf("%s:%s", key, value))
-	}
-	sort.Strings(keys)
-	return strings.Join(keys, ":")
-}
-
-func loadCacheServer(embedServer McpServer, config map[string]string) (Server, error) {
-	cacheKey := embedServer.ID
-	if len(config) > 0 {
-		cacheKey = fmt.Sprintf("%s:%s", embedServer.ID, buildNoReusingConfigCacheKey(config))
-	}
-	mcpServerCacheLock.RLock()
-	server, ok := mcpServerCache[cacheKey]
-	mcpServerCacheLock.RUnlock()
-	if ok {
-		server.LastUsedTimestamp.Store(time.Now().Unix())
-		return server.MCPServer, nil
-	}
-
-	mcpServerCacheLock.Lock()
-	defer mcpServerCacheLock.Unlock()
-	server, ok = mcpServerCache[cacheKey]
-	if ok {
-		server.LastUsedTimestamp.Store(time.Now().Unix())
-		return server.MCPServer, nil
-	}
-
-	mcpServer, err := embedServer.NewServer(config, nil)
-	if err != nil {
-		return nil, fmt.Errorf("mcp %s new server is invalid: %w", embedServer.ID, err)
-	}
-	mcpServerCacheItem := &mcpServerCacheItem{
-		MCPServer:         mcpServer,
-		LastUsedTimestamp: atomic.Int64{},
-	}
-	mcpServerCacheItem.LastUsedTimestamp.Store(time.Now().Unix())
-	mcpServerCache[cacheKey] = mcpServerCacheItem
-	return mcpServer, nil
-}
-
-func Servers() map[string]McpServer {
-	return servers
-}
-
-func GetEmbedMCP(id string) (McpServer, bool) {
-	mcp, ok := servers[id]
-	return mcp, ok
-}
-
-```
-
-File: mcp-servers/aiproxy-openapi/openapi.go
-```go
-package aiproxyopenapi
-
-import (
-	"fmt"
-	"net/url"
-	"sync"
-
-	"github.com/labring/aiproxy/core/docs"
-	mcpservers "github.com/labring/aiproxy/mcp-servers"
-	"github.com/labring/aiproxy/openapi-mcp/convert"
-)
-
-var configTemplates = map[string]mcpservers.ConfigTemplate{
-	"host": {
-		Name:        "Host",
-		Required:    mcpservers.ConfigRequiredTypeInitOnly,
-		Example:     "http://localhost:3000",
-		Description: "The host of the OpenAPI server",
-		Validator: func(value string) error {
-			u, err := url.Parse(value)
-			if err != nil {
-				return err
-			}
-			if u.Scheme != "http" && u.Scheme != "https" {
-				return fmt.Errorf("invalid scheme: %s", u.Scheme)
-			}
-			return nil
-		},
-	},
-
-	"authorization": {
-		Name:        "Authorization",
-		Required:    mcpservers.ConfigRequiredTypeReusingOptional,
-		Example:     "aiproxy-admin-key",
-		Description: "The admin key of the OpenAPI server",
-	},
-}
-
-var (
-	parser    *convert.Parser
-	parseOnce sync.Once
-)
-
-func getParser() *convert.Parser {
-	parseOnce.Do(func() {
-		parser = convert.NewParser()
-		err := parser.Parse([]byte(docs.SwaggerInfo.ReadDoc()))
-		if err != nil {
-			panic(err)
-		}
-	})
-	return parser
-}
-
-func NewServer(config, reusingConfig map[string]string) (mcpservers.Server, error) {
-	converter := convert.NewConverter(getParser(), convert.Options{
-		OpenAPIFrom:   config["host"],
-		Authorization: reusingConfig["authorization"],
-	})
-	return converter.Convert()
-}
-
-```
-
-File: mcp-servers/amap/main.go
-```go
-package amap
-
-import (
-	"context"
-	"errors"
-	"fmt"
-	"net/url"
-
-	mcpservers "github.com/labring/aiproxy/mcp-servers"
-	"github.com/mark3labs/mcp-go/client/transport"
-)
-
-var configTemplates = map[string]mcpservers.ConfigTemplate{
-	"key": {
-		Name:        "Key",
-		Required:    mcpservers.ConfigRequiredTypeInitOnly,
-		Example:     "1234567890",
-		Description: "The key of the AMap MCP server: https://console.amap.com/dev/key/app",
-	},
-
-	"url": {
-		Name:        "URL",
-		Required:    mcpservers.ConfigRequiredTypeInitOptional,
-		Example:     "https://mcp.amap.com/sse",
-		Description: "The URL of the AMap MCP server",
-	},
-}
-
-func NewServer(config, _ map[string]string) (mcpservers.Server, error) {
-	key := config["key"]
-	if key == "" {
-		return nil, errors.New("key is required")
-	}
-	u := config["url"]
-	if u == "" {
-		u = "https://mcp.amap.com/sse"
-	}
-
-	parsedURL, err := url.Parse(u)
-	if err != nil {
-		return nil, fmt.Errorf("invalid url: %w", err)
-	}
-	query := parsedURL.Query()
-	query.Set("key", key)
-	parsedURL.RawQuery = query.Encode()
-
-	client, err := transport.NewSSE(parsedURL.String())
-	if err != nil {
-		return nil, fmt.Errorf("failed to create sse client: %w", err)
-	}
-
-	err = client.Start(context.Background())
-	if err != nil {
-		return nil, fmt.Errorf("failed to start sse client: %w", err)
-	}
-
-	return mcpservers.WrapMCPClient2ServerWithCleanup(client), nil
-}
-
-```
-
-File: mcp-servers/amap/init.go
-```go
-package amap
-
-import mcpservers "github.com/labring/aiproxy/mcp-servers"
-
-// need import in mcpregister/init.go
-func init() {
-	mcpservers.Register(
-		mcpservers.NewMcp(
-			"amap",
-			"AMAP",
-			mcpservers.McpTypeEmbed,
-			mcpservers.WithNewServerFunc(NewServer),
-			mcpservers.WithConfigTemplates(configTemplates),
-			mcpservers.WithTags([]string{"map"}),
-			mcpservers.WithReadme(
-				`# AMAP MCP Server
-
-https://lbs.amap.com/api/mcp-server/gettingstarted
-`),
-		),
-	)
-}
-
-```
-
-File: mcp-servers/aiproxy-openapi/init.go
-```go
-package aiproxyopenapi
-
-import mcpservers "github.com/labring/aiproxy/mcp-servers"
-
-// need import in mcpregister/init.go
-func init() {
-	mcpservers.Register(
-		mcpservers.NewMcp(
-			"aiproxy-openapi",
-			"AI Proxy OpenAPI",
-			mcpservers.McpTypeEmbed,
-			mcpservers.WithNewServerFunc(NewServer),
-			mcpservers.WithConfigTemplates(configTemplates),
-		),
-	)
-}
-
-```
-
-File: mcp-servers/mcpregister/init.go
-```go
-package mcpregister
-
-import (
-	// register embed mcp
-	_ "github.com/labring/aiproxy/mcp-servers/aiproxy-openapi"
-	_ "github.com/labring/aiproxy/mcp-servers/alipay"
-	_ "github.com/labring/aiproxy/mcp-servers/amap"
-	_ "github.com/labring/aiproxy/mcp-servers/web-search"
-)
-
-```
-
-
-这是后端的代码,帮我加一个页面,页面有一个tab可以切换三个子页面。第一个页面展示所有的mcp列表、并展示sse和sstreamhttp的地址。第二个页面展示所有内置的mcp服务器、并提供配置参数、保存启用等功能。第三个页面展示mcp服务器配置功能、可以修改参数、添加mcp后端等操作