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

feat: add group mcp detail and endpoint (#266)

* feat: add group mcp detail and endpoint

* fix: ci lint

* feat: add is hosted filed

* chore: readme omit empty
zijiren 6 месяцев назад
Родитель
Сommit
bf9a7e28b8

+ 53 - 9
core/controller/mcp/embedmcp.go

@@ -60,6 +60,7 @@ type EmbedMCP struct {
 	ID              string                    `json:"id"`
 	Enabled         bool                      `json:"enabled"`
 	Name            string                    `json:"name"`
+	NameCN          string                    `json:"name_cn"`
 	Readme          string                    `json:"readme"`
 	ReadmeURL       string                    `json:"readme_url"`
 	ReadmeCN        string                    `json:"readme_cn"`
@@ -79,6 +80,7 @@ func newEmbedMCP(
 		ID:          mcp.ID,
 		Enabled:     enabled,
 		Name:        mcp.Name,
+		NameCN:      mcp.NameCN,
 		Readme:      mcp.Readme,
 		ReadmeURL:   mcp.ReadmeURL,
 		ReadmeCN:    mcp.ReadmeCN,
@@ -220,22 +222,48 @@ func GetProxyConfig(
 			value = param.Default
 		}
 
-		switch param.Type {
-		case model.ParamTypeURL:
+		switch param.Required {
+		case mcpservers.ConfigRequiredTypeInitOnly:
+			// 必须在初始化时提供
 			if value == "" {
-				return nil, fmt.Errorf("url parameter %s is required", key)
+				return nil, fmt.Errorf("parameter %s is required", key)
 			}
-			config.URL = value
-		case model.ParamTypeHeader:
+			applyParamToConfig(config, key, value, param.Type)
+		case mcpservers.ConfigRequiredTypeReusingOnly:
+			// 只能通过 reusing 提供,不能在初始化时提供
 			if value != "" {
-				config.Headers[key] = value
+				return nil, fmt.Errorf(
+					"parameter %s should not be provided in init config, it should be provided via reusing",
+					key,
+				)
+			}
+			config.Reusing[key] = model.PublicMCPProxyReusingParam{
+				ReusingParam: model.ReusingParam{
+					Name:        param.Name,
+					Description: param.Description,
+					Required:    true,
+				},
+				Type: param.Type,
 			}
-		case model.ParamTypeQuery:
+		case mcpservers.ConfigRequiredTypeInitOrReusingOnly:
+			// 可以在初始化时提供,也可以通过 reusing 提供
 			if value != "" {
-				config.Querys[key] = value
+				applyParamToConfig(config, key, value, param.Type)
+			} else {
+				config.Reusing[key] = model.PublicMCPProxyReusingParam{
+					ReusingParam: model.ReusingParam{
+						Name:        param.Name,
+						Description: param.Description,
+						Required:    true,
+					},
+					Type: param.Type,
+				}
 			}
 		default:
-			return nil, fmt.Errorf("unsupported proxy param type: %s", param.Type)
+			// 可选参数
+			if value != "" {
+				applyParamToConfig(config, key, value, param.Type)
+			}
 		}
 	}
 
@@ -246,6 +274,22 @@ func GetProxyConfig(
 	return config, nil
 }
 
+// 辅助函数:将参数应用到配置中
+func applyParamToConfig(
+	config *model.PublicMCPProxyConfig,
+	key, value string,
+	paramType model.ProxyParamType,
+) {
+	switch paramType {
+	case model.ParamTypeURL:
+		config.URL = value
+	case model.ParamTypeHeader:
+		config.Headers[key] = value
+	case model.ParamTypeQuery:
+		config.Querys[key] = value
+	}
+}
+
 func ToPublicMCP(
 	e mcpservers.McpServer,
 	initConfig map[string]string,

+ 23 - 18
core/controller/mcp/mcp.go

@@ -82,32 +82,37 @@ func handleEmbedSSEMCP(
 	config *model.MCPEmbeddingConfig,
 	endpoint EndpointProvider,
 ) {
-	var reusingConfig map[string]string
-	if len(config.Reusing) != 0 {
-		group := middleware.GetGroup(c)
-		param, err := model.CacheGetPublicMCPReusingParam(mcpID, group.ID)
-		if err != nil {
-			c.JSON(http.StatusBadRequest, mcpservers.CreateMCPErrorResponse(
-				mcp.NewRequestId(nil),
-				mcp.INVALID_REQUEST,
-				err.Error(),
-			))
-			return
-		}
-		reusingConfig = param.Params
+	reusingConfig, err := prepareEmbedReusingConfig(c, mcpID, config.Reusing)
+	if err != nil {
+		http.Error(c.Writer, err.Error(), http.StatusBadRequest)
+		return
 	}
+
 	server, err := mcpservers.GetMCPServer(mcpID, config.Init, reusingConfig)
 	if err != nil {
-		c.JSON(http.StatusBadRequest, mcpservers.CreateMCPErrorResponse(
-			mcp.NewRequestId(nil),
-			mcp.INVALID_REQUEST,
-			err.Error(),
-		))
+		http.Error(c.Writer, err.Error(), http.StatusBadRequest)
 		return
 	}
+
 	handleSSEMCPServer(c, server, string(model.PublicMCPTypeEmbed), endpoint)
 }
 
+// prepareEmbedReusingConfig 准备嵌入MCP的reusing配置
+func prepareEmbedReusingConfig(
+	c *gin.Context,
+	mcpID string,
+	reusingParams map[string]model.ReusingParam,
+) (map[string]string, error) {
+	if len(reusingParams) == 0 {
+		return nil, nil
+	}
+
+	group := middleware.GetGroup(c)
+	processor := NewReusingParamProcessor(mcpID, group.ID)
+
+	return processor.ProcessEmbedReusingParams(reusingParams)
+}
+
 func sendMCPSSEMessage(c *gin.Context, mcpType, sessionID string) {
 	backend, ok := getStore().Get(sessionID)
 	if !ok || backend != mcpType {

+ 123 - 31
core/controller/mcp/publicmcp-group.go

@@ -9,22 +9,80 @@ import (
 	"github.com/labring/aiproxy/core/controller/utils"
 	"github.com/labring/aiproxy/core/middleware"
 	"github.com/labring/aiproxy/core/model"
+	"github.com/mark3labs/mcp-go/mcp"
 	"gorm.io/gorm"
 )
 
+func IsHostedMCP(t model.PublicMCPType) bool {
+	return t == model.PublicMCPTypeEmbed ||
+		t == model.PublicMCPTypeOpenAPI ||
+		t == model.PublicMCPTypeProxySSE ||
+		t == model.PublicMCPTypeProxyStreamable
+}
+
 type GroupPublicMCPResponse struct {
 	model.PublicMCP
+	Hosted bool `json:"hosted"`
+}
+
+func (r *GroupPublicMCPResponse) MarshalJSON() ([]byte, error) {
+	type Alias GroupPublicMCPResponse
+	a := &struct {
+		*Alias
+		CreatedAt int64 `json:"created_at,omitempty"`
+		UpdateAt  int64 `json:"update_at,omitempty"`
+	}{
+		Alias: (*Alias)(r),
+	}
+	if !r.CreatedAt.IsZero() {
+		a.CreatedAt = r.CreatedAt.UnixMilli()
+	}
+	if !r.UpdateAt.IsZero() {
+		a.UpdateAt = r.UpdateAt.UnixMilli()
+	}
+	return sonic.Marshal(a)
+}
+
+func NewGroupPublicMCPResponse(mcp model.PublicMCP) GroupPublicMCPResponse {
+	r := GroupPublicMCPResponse{
+		PublicMCP: mcp,
+		Hosted:    IsHostedMCP(mcp.Type),
+	}
+	r.Type = ""
+	r.Readme = ""
+	r.ReadmeCN = ""
+	r.ReadmeURL = ""
+	r.ReadmeCNURL = ""
+	r.ProxyConfig = nil
+	r.EmbedConfig = nil
+	r.OpenAPIConfig = nil
+	r.TestConfig = nil
+	return r
+}
+
+func NewGroupPublicMCPResponses(mcps []model.PublicMCP) []GroupPublicMCPResponse {
+	responses := make([]GroupPublicMCPResponse, len(mcps))
+	for i, mcp := range mcps {
+		responses[i] = NewGroupPublicMCPResponse(mcp)
+	}
+	return responses
+}
+
+type GroupPublicMCPDetailResponse struct {
+	model.PublicMCP
+	Hosted    bool                          `json:"hosted"`
 	Endpoints MCPEndpoint                   `json:"endpoints"`
 	Reusing   map[string]model.ReusingParam `json:"reusing"`
 	Params    map[string]string             `json:"params"`
+	Tools     []mcp.Tool                    `json:"tools"`
 }
 
-func (r *GroupPublicMCPResponse) MarshalJSON() ([]byte, error) {
-	type Alias GroupPublicMCPResponse
+func (r *GroupPublicMCPDetailResponse) MarshalJSON() ([]byte, error) {
+	type Alias GroupPublicMCPDetailResponse
 	a := &struct {
 		*Alias
-		CreatedAt int64 `json:"created_at"`
-		UpdateAt  int64 `json:"update_at"`
+		CreatedAt int64 `json:"created_at,omitempty"`
+		UpdateAt  int64 `json:"update_at,omitempty"`
 	}{
 		Alias: (*Alias)(r),
 	}
@@ -37,18 +95,33 @@ func (r *GroupPublicMCPResponse) MarshalJSON() ([]byte, error) {
 	return sonic.Marshal(a)
 }
 
-func NewGroupPublicMCPResponse(
+func checkParamsIsFull(params model.Params, reusing map[string]model.ReusingParam) bool {
+	for _, r := range reusing {
+		if !r.Required {
+			continue
+		}
+		if v, ok := params[r.Name]; !ok || v == "" {
+			return false
+		}
+	}
+	return true
+}
+
+func NewGroupPublicMCPDetailResponse(
 	host string,
 	mcp model.PublicMCP,
 	groupID string,
-) (GroupPublicMCPResponse, error) {
-	r := GroupPublicMCPResponse{
+) (GroupPublicMCPDetailResponse, error) {
+	r := GroupPublicMCPDetailResponse{
 		PublicMCP: mcp,
-		Endpoints: NewPublicMCPEndpoint(host, mcp),
+		Hosted:    IsHostedMCP(mcp.Type),
 	}
+
+	r.Type = ""
 	r.ProxyConfig = nil
 	r.EmbedConfig = nil
 	r.OpenAPIConfig = nil
+	r.TestConfig = nil
 
 	switch mcp.Type {
 	case model.PublicMCPTypeProxySSE, model.PublicMCPTypeProxyStreamable:
@@ -57,6 +130,8 @@ func NewGroupPublicMCPResponse(
 		}
 	case model.PublicMCPTypeEmbed:
 		r.Reusing = mcp.EmbedConfig.Reusing
+	default:
+		return r, nil
 	}
 
 	reusingParams, err := model.GetPublicMCPReusingParam(mcp.ID, groupID)
@@ -67,23 +142,11 @@ func NewGroupPublicMCPResponse(
 	}
 	r.Params = reusingParams.Params
 
-	return r, nil
-}
-
-func NewGroupPublicMCPResponses(
-	host string,
-	mcps []model.PublicMCP,
-	groupID string,
-) ([]GroupPublicMCPResponse, error) {
-	responses := make([]GroupPublicMCPResponse, len(mcps))
-	for i, mcp := range mcps {
-		response, err := NewGroupPublicMCPResponse(host, mcp, groupID)
-		if err != nil {
-			return nil, err
-		}
-		responses[i] = response
+	if checkParamsIsFull(r.Params, r.Reusing) {
+		r.Endpoints = NewPublicMCPEndpoint(host, mcp)
 	}
-	return responses, nil
+
+	return r, nil
 }
 
 // GetGroupPublicMCPs godoc
@@ -102,8 +165,6 @@ func NewGroupPublicMCPResponses(
 //	@Success		200			{object}	middleware.APIResponse{data=[]GroupPublicMCPResponse}
 //	@Router			/api/group/{group}/mcp [get]
 func GetGroupPublicMCPs(c *gin.Context) {
-	groupID := c.Param("group")
-
 	page, perPage := utils.ParsePageParams(c)
 	mcpType := model.PublicMCPType(c.Query("type"))
 	keyword := c.Query("keyword")
@@ -120,14 +181,45 @@ func GetGroupPublicMCPs(c *gin.Context) {
 		return
 	}
 
-	responses, err := NewGroupPublicMCPResponses(c.Request.Host, mcps, groupID)
-	if err != nil {
-		middleware.ErrorResponse(c, http.StatusInternalServerError, err.Error())
-		return
-	}
+	responses := NewGroupPublicMCPResponses(mcps)
 
 	middleware.SuccessResponse(c, gin.H{
 		"mcps":  responses,
 		"total": total,
 	})
 }
+
+// GetGroupPublicMCPByID godoc
+//
+//	@Summary		Get MCP by ID
+//	@Description	Get a specific MCP by its ID
+//	@Tags			mcp
+//	@Tags			group
+//	@Produce		json
+//	@Security		ApiKeyAuth
+//	@Param			group	path		string	true	"Group ID"
+//	@Param			id		path		string	true	"MCP ID"
+//	@Success		200		{object}	middleware.APIResponse{data=GroupPublicMCPDetailResponse}
+//	@Router			/api/group/{group}/mcp/{id} [get]
+func GetGroupPublicMCPByID(c *gin.Context) {
+	groupID := c.Param("group")
+	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
+	}
+
+	response, err := NewGroupPublicMCPDetailResponse(c.Request.Host, mcp, groupID)
+	if err != nil {
+		middleware.ErrorResponse(c, http.StatusInternalServerError, err.Error())
+		return
+	}
+
+	middleware.SuccessResponse(c, response)
+}

+ 136 - 69
core/controller/mcp/publicmcp-server.go

@@ -1,7 +1,6 @@
 package controller
 
 import (
-	"errors"
 	"fmt"
 	"net/http"
 	"net/url"
@@ -76,72 +75,153 @@ func handlePublicSSEMCP(
 ) {
 	switch publicMcp.Type {
 	case model.PublicMCPTypeProxySSE:
-		client, err := transport.NewSSE(
-			publicMcp.ProxyConfig.URL,
-			transport.WithHeaders(publicMcp.ProxyConfig.Headers),
-		)
-		if err != nil {
+		if err := handlePublicProxySSE(c, publicMcp, endpoint); err != nil {
 			http.Error(c.Writer, err.Error(), http.StatusBadRequest)
 			return
 		}
-		err = client.Start(c.Request.Context())
-		if err != nil {
-			http.Error(c.Writer, err.Error(), http.StatusBadRequest)
-			return
-		}
-		defer client.Close()
-		handleSSEMCPServer(
-			c,
-			mcpservers.WrapMCPClient2Server(client),
-			string(model.PublicMCPTypeProxySSE),
-			endpoint,
-		)
 	case model.PublicMCPTypeProxyStreamable:
-		client, err := transport.NewStreamableHTTP(
-			publicMcp.ProxyConfig.URL,
-			transport.WithHTTPHeaders(publicMcp.ProxyConfig.Headers),
-		)
-		if err != nil {
+		if err := handlePublicProxyStreamableSSE(c, publicMcp, endpoint); err != nil {
 			http.Error(c.Writer, err.Error(), http.StatusBadRequest)
 			return
 		}
-		err = client.Start(c.Request.Context())
-		if err != nil {
-			http.Error(c.Writer, err.Error(), http.StatusBadRequest)
-			return
-		}
-		defer client.Close()
-		handleSSEMCPServer(
-			c,
-			mcpservers.WrapMCPClient2Server(client),
-			string(model.PublicMCPTypeProxyStreamable),
-			endpoint,
-		)
 	case model.PublicMCPTypeOpenAPI:
 		server, err := newOpenAPIMCPServer(publicMcp.OpenAPIConfig)
 		if err != nil {
-			c.JSON(http.StatusBadRequest, mcpservers.CreateMCPErrorResponse(
-				mcp.NewRequestId(nil),
-				mcp.INVALID_REQUEST,
-				err.Error(),
-			))
+			http.Error(c.Writer, err.Error(), http.StatusBadRequest)
 			return
 		}
 		handleSSEMCPServer(c, server, string(model.PublicMCPTypeOpenAPI), endpoint)
 	case model.PublicMCPTypeEmbed:
 		handleEmbedSSEMCP(c, publicMcp.ID, publicMcp.EmbedConfig, endpoint)
 	default:
-		c.JSON(http.StatusBadRequest, mcpservers.CreateMCPErrorResponse(
-			mcp.NewRequestId(nil),
-			mcp.INVALID_REQUEST,
-			"unknown mcp type",
-		))
-		return
+		http.Error(c.Writer, "unknown mcp type", http.StatusBadRequest)
 	}
 }
 
-// processReusingParams handles the reusing parameters for MCP proxy
-func processReusingParams(
+// handlePublicProxySSE 处理公共代理SSE
+func handlePublicProxySSE(
+	c *gin.Context,
+	publicMcp *model.PublicMCPCache,
+	endpoint EndpointProvider,
+) error {
+	client, err := createProxySSEClient(c, publicMcp)
+	if err != nil {
+		return err
+	}
+	defer client.Close()
+
+	handleSSEMCPServer(
+		c,
+		mcpservers.WrapMCPClient2Server(client),
+		string(model.PublicMCPTypeProxySSE),
+		endpoint,
+	)
+	return nil
+}
+
+// handlePublicProxyStreamableSSE 处理公共代理Streamable SSE
+func handlePublicProxyStreamableSSE(
+	c *gin.Context,
+	publicMcp *model.PublicMCPCache,
+	endpoint EndpointProvider,
+) error {
+	client, err := createProxyStreamableClient(c, publicMcp)
+	if err != nil {
+		return err
+	}
+	defer client.Close()
+
+	handleSSEMCPServer(
+		c,
+		mcpservers.WrapMCPClient2Server(client),
+		string(model.PublicMCPTypeProxyStreamable),
+		endpoint,
+	)
+	return nil
+}
+
+// createProxySSEClient 创建代理SSE客户端
+func createProxySSEClient(
+	c *gin.Context,
+	publicMcp *model.PublicMCPCache,
+) (transport.Interface, error) {
+	url, headers, err := prepareProxyConfig(c, publicMcp)
+	if err != nil {
+		return nil, err
+	}
+
+	client, err := transport.NewSSE(url, transport.WithHeaders(headers))
+	if err != nil {
+		return nil, err
+	}
+
+	if err := client.Start(c.Request.Context()); err != nil {
+		return nil, err
+	}
+
+	return client, nil
+}
+
+// createProxyStreamableClient 创建代理Streamable客户端
+func createProxyStreamableClient(
+	c *gin.Context,
+	publicMcp *model.PublicMCPCache,
+) (transport.Interface, error) {
+	url, headers, err := prepareProxyConfig(c, publicMcp)
+	if err != nil {
+		return nil, err
+	}
+
+	client, err := transport.NewStreamableHTTP(url, transport.WithHTTPHeaders(headers))
+	if err != nil {
+		return nil, err
+	}
+
+	if err := client.Start(c.Request.Context()); err != nil {
+		return nil, err
+	}
+
+	return client, nil
+}
+
+// prepareProxyConfig 准备代理配置
+func prepareProxyConfig(
+	c *gin.Context,
+	publicMcp *model.PublicMCPCache,
+) (string, map[string]string, error) {
+	url, err := url.Parse(publicMcp.ProxyConfig.URL)
+	if err != nil {
+		return "", nil, fmt.Errorf("invalid proxy URL: %w", err)
+	}
+
+	headers := make(map[string]string)
+	backendQuery := url.Query()
+
+	// 复制静态配置
+	for k, v := range publicMcp.ProxyConfig.Headers {
+		headers[k] = v
+	}
+
+	// 处理reusing参数
+	if len(publicMcp.ProxyConfig.Reusing) > 0 {
+		group := middleware.GetGroup(c)
+		processor := NewReusingParamProcessor(publicMcp.ID, group.ID)
+
+		if err := processor.ProcessProxyReusingParams(
+			publicMcp.ProxyConfig.Reusing,
+			headers,
+			&backendQuery,
+		); err != nil {
+			return "", nil, err
+		}
+	}
+
+	url.RawQuery = backendQuery.Encode()
+	return url.String(), headers, nil
+}
+
+// processProxyReusingParams handles the reusing parameters for MCP proxy
+func processProxyReusingParams(
 	reusingParams map[string]model.PublicMCPProxyReusingParam,
 	mcpID, groupID string,
 	headers map[string]string,
@@ -160,7 +240,7 @@ func processReusingParams(
 		paramValue, ok := param.Params[k]
 		if !ok {
 			if v.Required {
-				return fmt.Errorf("%s required", k)
+				return fmt.Errorf("required reusing parameter %s is missing", k)
 			}
 			continue
 		}
@@ -170,8 +250,10 @@ func processReusingParams(
 			headers[k] = paramValue
 		case model.ParamTypeQuery:
 			backendQuery.Set(k, paramValue)
+		case model.ParamTypeURL:
+			return fmt.Errorf("URL parameter %s cannot be set via reusing", k)
 		default:
-			return errors.New("unknow param type")
+			return fmt.Errorf("unknown param type: %s", v.Type)
 		}
 	}
 
@@ -247,28 +329,13 @@ func PublicMCPStreamable(c *gin.Context) {
 func handlePublicStreamable(c *gin.Context, publicMcp *model.PublicMCPCache) {
 	switch publicMcp.Type {
 	case model.PublicMCPTypeProxySSE:
-		client, err := transport.NewSSE(
-			publicMcp.ProxyConfig.URL,
-			transport.WithHeaders(publicMcp.ProxyConfig.Headers),
-		)
-		if err != nil {
-			c.JSON(http.StatusBadRequest, mcpservers.CreateMCPErrorResponse(
-				mcp.NewRequestId(nil),
-				mcp.INVALID_REQUEST,
-				err.Error(),
-			))
-			return
-		}
-		err = client.Start(c.Request.Context())
+		client, err := createProxySSEClient(c, publicMcp)
 		if err != nil {
-			c.JSON(http.StatusBadRequest, mcpservers.CreateMCPErrorResponse(
-				mcp.NewRequestId(nil),
-				mcp.INVALID_REQUEST,
-				err.Error(),
-			))
+			http.Error(c.Writer, err.Error(), http.StatusBadRequest)
 			return
 		}
 		defer client.Close()
+
 		mcpproxy.NewStatelessStreamableHTTPServer(
 			mcpservers.WrapMCPClient2Server(client),
 		).ServeHTTP(c.Writer, c.Request)
@@ -349,7 +416,7 @@ func handlePublicProxyStreamable(c *gin.Context, mcpID string, config *model.Pub
 	group := middleware.GetGroup(c)
 
 	// Process reusing parameters if any
-	if err := processReusingParams(config.Reusing, mcpID, group.ID, headers, &backendQuery); err != nil {
+	if err := processProxyReusingParams(config.Reusing, mcpID, group.ID, headers, &backendQuery); err != nil {
 		c.JSON(http.StatusBadRequest, mcpservers.CreateMCPErrorResponse(
 			mcp.NewRequestId(nil),
 			mcp.INVALID_REQUEST,

+ 101 - 0
core/controller/mcp/reusing.go

@@ -0,0 +1,101 @@
+package controller
+
+import (
+	"fmt"
+	"net/url"
+
+	"github.com/labring/aiproxy/core/model"
+)
+
+// ReusingParamProcessor 统一处理reusing参数
+type ReusingParamProcessor struct {
+	mcpID   string
+	groupID string
+}
+
+func NewReusingParamProcessor(mcpID, groupID string) *ReusingParamProcessor {
+	return &ReusingParamProcessor{
+		mcpID:   mcpID,
+		groupID: groupID,
+	}
+}
+
+// ProcessProxyReusingParams 处理代理类型的reusing参数
+func (p *ReusingParamProcessor) ProcessProxyReusingParams(
+	reusingParams map[string]model.PublicMCPProxyReusingParam,
+	headers map[string]string,
+	backendQuery *url.Values,
+) error {
+	if len(reusingParams) == 0 {
+		return nil
+	}
+
+	param, err := model.CacheGetPublicMCPReusingParam(p.mcpID, p.groupID)
+	if err != nil {
+		return fmt.Errorf("failed to get reusing params: %w", err)
+	}
+
+	for key, config := range reusingParams {
+		value, exists := param.Params[key]
+		if !exists {
+			if config.Required {
+				return fmt.Errorf("required reusing parameter %s is missing", key)
+			}
+			continue
+		}
+
+		if err := p.applyProxyParam(key, value, config.Type, headers, backendQuery); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+// ProcessEmbedReusingParams 处理嵌入类型的reusing参数
+func (p *ReusingParamProcessor) ProcessEmbedReusingParams(
+	reusingParams map[string]model.ReusingParam,
+) (map[string]string, error) {
+	if len(reusingParams) == 0 {
+		return nil, nil
+	}
+
+	param, err := model.CacheGetPublicMCPReusingParam(p.mcpID, p.groupID)
+	if err != nil {
+		return nil, fmt.Errorf("failed to get reusing params: %w", err)
+	}
+
+	reusingConfig := make(map[string]string)
+	for key, config := range reusingParams {
+		value, exists := param.Params[key]
+		if !exists {
+			if config.Required {
+				return nil, fmt.Errorf("required reusing parameter %s is missing", key)
+			}
+			continue
+		}
+		reusingConfig[key] = value
+	}
+
+	return reusingConfig, nil
+}
+
+// applyProxyParam 应用代理参数到相应位置
+func (p *ReusingParamProcessor) applyProxyParam(
+	key, value string,
+	paramType model.ProxyParamType,
+	headers map[string]string,
+	backendQuery *url.Values,
+) error {
+	switch paramType {
+	case model.ParamTypeHeader:
+		headers[key] = value
+	case model.ParamTypeQuery:
+		backendQuery.Set(key, value)
+	case model.ParamTypeURL:
+		return fmt.Errorf("URL parameter %s cannot be set via reusing", key)
+	default:
+		return fmt.Errorf("unknown param type: %s", paramType)
+	}
+	return nil
+}

+ 243 - 5
core/docs/docs.go

@@ -1619,6 +1619,60 @@ const docTemplate = `{
                 }
             }
         },
+        "/api/group/{group}/mcp/{id}": {
+            "get": {
+                "security": [
+                    {
+                        "ApiKeyAuth": []
+                    }
+                ],
+                "description": "Get a specific MCP by its ID",
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "mcp",
+                    "group"
+                ],
+                "summary": "Get MCP by ID",
+                "parameters": [
+                    {
+                        "type": "string",
+                        "description": "Group ID",
+                        "name": "group",
+                        "in": "path",
+                        "required": true
+                    },
+                    {
+                        "type": "string",
+                        "description": "MCP ID",
+                        "name": "id",
+                        "in": "path",
+                        "required": true
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "allOf": [
+                                {
+                                    "$ref": "#/definitions/middleware.APIResponse"
+                                },
+                                {
+                                    "type": "object",
+                                    "properties": {
+                                        "data": {
+                                            "$ref": "#/definitions/controller.GroupPublicMCPDetailResponse"
+                                        }
+                                    }
+                                }
+                            ]
+                        }
+                    }
+                }
+            }
+        },
         "/api/group/{group}/model_config/{model}": {
             "get": {
                 "security": [
@@ -8230,6 +8284,9 @@ const docTemplate = `{
                 "name": {
                     "type": "string"
                 },
+                "name_cn": {
+                    "type": "string"
+                },
                 "readme": {
                     "type": "string"
                 },
@@ -8333,7 +8390,7 @@ const docTemplate = `{
                 }
             }
         },
-        "controller.GroupPublicMCPResponse": {
+        "controller.GroupPublicMCPDetailResponse": {
             "type": "object",
             "properties": {
                 "created_at": {
@@ -8363,6 +8420,9 @@ const docTemplate = `{
                 "name": {
                     "type": "string"
                 },
+                "name_cn": {
+                    "type": "string"
+                },
                 "openapi_config": {
                     "$ref": "#/definitions/model.MCPOpenAPIConfig"
                 },
@@ -8405,6 +8465,86 @@ const docTemplate = `{
                         "type": "string"
                     }
                 },
+                "test_config": {
+                    "$ref": "#/definitions/model.TestConfig"
+                },
+                "tools": {
+                    "type": "array",
+                    "items": {
+                        "$ref": "#/definitions/mcp.Tool"
+                    }
+                },
+                "type": {
+                    "$ref": "#/definitions/model.PublicMCPType"
+                },
+                "update_at": {
+                    "type": "string"
+                }
+            }
+        },
+        "controller.GroupPublicMCPResponse": {
+            "type": "object",
+            "properties": {
+                "created_at": {
+                    "type": "string"
+                },
+                "description": {
+                    "type": "string"
+                },
+                "description_cn": {
+                    "type": "string"
+                },
+                "embed_config": {
+                    "$ref": "#/definitions/model.MCPEmbeddingConfig"
+                },
+                "github_url": {
+                    "type": "string"
+                },
+                "id": {
+                    "type": "string"
+                },
+                "logo_url": {
+                    "type": "string"
+                },
+                "name": {
+                    "type": "string"
+                },
+                "name_cn": {
+                    "type": "string"
+                },
+                "openapi_config": {
+                    "$ref": "#/definitions/model.MCPOpenAPIConfig"
+                },
+                "price": {
+                    "$ref": "#/definitions/model.MCPPrice"
+                },
+                "proxy_config": {
+                    "$ref": "#/definitions/model.PublicMCPProxyConfig"
+                },
+                "readme": {
+                    "type": "string"
+                },
+                "readme_cn": {
+                    "type": "string"
+                },
+                "readme_cn_url": {
+                    "type": "string"
+                },
+                "readme_url": {
+                    "type": "string"
+                },
+                "status": {
+                    "$ref": "#/definitions/model.PublicMCPStatus"
+                },
+                "tags": {
+                    "type": "array",
+                    "items": {
+                        "type": "string"
+                    }
+                },
+                "test_config": {
+                    "$ref": "#/definitions/model.TestConfig"
+                },
                 "type": {
                     "$ref": "#/definitions/model.PublicMCPType"
                 },
@@ -8576,6 +8716,9 @@ const docTemplate = `{
                 "name": {
                     "type": "string"
                 },
+                "name_cn": {
+                    "type": "string"
+                },
                 "openapi_config": {
                     "$ref": "#/definitions/model.MCPOpenAPIConfig"
                 },
@@ -8606,6 +8749,9 @@ const docTemplate = `{
                         "type": "string"
                     }
                 },
+                "test_config": {
+                    "$ref": "#/definitions/model.TestConfig"
+                },
                 "type": {
                     "$ref": "#/definitions/model.PublicMCPType"
                 },
@@ -8948,6 +9094,78 @@ const docTemplate = `{
                 }
             }
         },
+        "mcp.Tool": {
+            "type": "object",
+            "properties": {
+                "annotations": {
+                    "description": "Optional properties describing tool behavior",
+                    "allOf": [
+                        {
+                            "$ref": "#/definitions/mcp.ToolAnnotation"
+                        }
+                    ]
+                },
+                "description": {
+                    "description": "A human-readable description of the tool.",
+                    "type": "string"
+                },
+                "inputSchema": {
+                    "description": "A JSON Schema object defining the expected parameters for the tool.",
+                    "allOf": [
+                        {
+                            "$ref": "#/definitions/mcp.ToolInputSchema"
+                        }
+                    ]
+                },
+                "name": {
+                    "description": "The name of the tool.",
+                    "type": "string"
+                }
+            }
+        },
+        "mcp.ToolAnnotation": {
+            "type": "object",
+            "properties": {
+                "destructiveHint": {
+                    "description": "If true, the tool may perform destructive updates",
+                    "type": "boolean"
+                },
+                "idempotentHint": {
+                    "description": "If true, repeated calls with same args have no additional effect",
+                    "type": "boolean"
+                },
+                "openWorldHint": {
+                    "description": "If true, tool interacts with external entities",
+                    "type": "boolean"
+                },
+                "readOnlyHint": {
+                    "description": "If true, the tool does not modify its environment",
+                    "type": "boolean"
+                },
+                "title": {
+                    "description": "Human-readable title for the tool",
+                    "type": "string"
+                }
+            }
+        },
+        "mcp.ToolInputSchema": {
+            "type": "object",
+            "properties": {
+                "properties": {
+                    "type": "object",
+                    "additionalProperties": {}
+                },
+                "required": {
+                    "type": "array",
+                    "items": {
+                        "type": "string"
+                    }
+                },
+                "type": {
+                    "type": "string"
+                }
+            }
+        },
         "middleware.APIResponse": {
             "type": "object",
             "properties": {
@@ -10251,6 +10469,12 @@ const docTemplate = `{
                 }
             }
         },
+        "model.Params": {
+            "type": "object",
+            "additionalProperties": {
+                "type": "string"
+            }
+        },
         "model.ParsePdfResponse": {
             "type": "object",
             "properties": {
@@ -10367,6 +10591,9 @@ const docTemplate = `{
                 "name": {
                     "type": "string"
                 },
+                "name_cn": {
+                    "type": "string"
+                },
                 "openapi_config": {
                     "$ref": "#/definitions/model.MCPOpenAPIConfig"
                 },
@@ -10397,6 +10624,9 @@ const docTemplate = `{
                         "type": "string"
                     }
                 },
+                "test_config": {
+                    "$ref": "#/definitions/model.TestConfig"
+                },
                 "type": {
                     "$ref": "#/definitions/model.PublicMCPType"
                 },
@@ -10461,10 +10691,7 @@ const docTemplate = `{
                     "type": "string"
                 },
                 "params": {
-                    "type": "object",
-                    "additionalProperties": {
-                        "type": "string"
-                    }
+                    "$ref": "#/definitions/model.Params"
                 },
                 "update_at": {
                     "type": "string"
@@ -10701,6 +10928,17 @@ const docTemplate = `{
                 }
             }
         },
+        "model.TestConfig": {
+            "type": "object",
+            "properties": {
+                "enabled": {
+                    "type": "boolean"
+                },
+                "params": {
+                    "$ref": "#/definitions/model.Params"
+                }
+            }
+        },
         "model.TextResponse": {
             "type": "object",
             "properties": {

+ 243 - 5
core/docs/swagger.json

@@ -1610,6 +1610,60 @@
                 }
             }
         },
+        "/api/group/{group}/mcp/{id}": {
+            "get": {
+                "security": [
+                    {
+                        "ApiKeyAuth": []
+                    }
+                ],
+                "description": "Get a specific MCP by its ID",
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "mcp",
+                    "group"
+                ],
+                "summary": "Get MCP by ID",
+                "parameters": [
+                    {
+                        "type": "string",
+                        "description": "Group ID",
+                        "name": "group",
+                        "in": "path",
+                        "required": true
+                    },
+                    {
+                        "type": "string",
+                        "description": "MCP ID",
+                        "name": "id",
+                        "in": "path",
+                        "required": true
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "allOf": [
+                                {
+                                    "$ref": "#/definitions/middleware.APIResponse"
+                                },
+                                {
+                                    "type": "object",
+                                    "properties": {
+                                        "data": {
+                                            "$ref": "#/definitions/controller.GroupPublicMCPDetailResponse"
+                                        }
+                                    }
+                                }
+                            ]
+                        }
+                    }
+                }
+            }
+        },
         "/api/group/{group}/model_config/{model}": {
             "get": {
                 "security": [
@@ -8221,6 +8275,9 @@
                 "name": {
                     "type": "string"
                 },
+                "name_cn": {
+                    "type": "string"
+                },
                 "readme": {
                     "type": "string"
                 },
@@ -8324,7 +8381,7 @@
                 }
             }
         },
-        "controller.GroupPublicMCPResponse": {
+        "controller.GroupPublicMCPDetailResponse": {
             "type": "object",
             "properties": {
                 "created_at": {
@@ -8354,6 +8411,9 @@
                 "name": {
                     "type": "string"
                 },
+                "name_cn": {
+                    "type": "string"
+                },
                 "openapi_config": {
                     "$ref": "#/definitions/model.MCPOpenAPIConfig"
                 },
@@ -8396,6 +8456,86 @@
                         "type": "string"
                     }
                 },
+                "test_config": {
+                    "$ref": "#/definitions/model.TestConfig"
+                },
+                "tools": {
+                    "type": "array",
+                    "items": {
+                        "$ref": "#/definitions/mcp.Tool"
+                    }
+                },
+                "type": {
+                    "$ref": "#/definitions/model.PublicMCPType"
+                },
+                "update_at": {
+                    "type": "string"
+                }
+            }
+        },
+        "controller.GroupPublicMCPResponse": {
+            "type": "object",
+            "properties": {
+                "created_at": {
+                    "type": "string"
+                },
+                "description": {
+                    "type": "string"
+                },
+                "description_cn": {
+                    "type": "string"
+                },
+                "embed_config": {
+                    "$ref": "#/definitions/model.MCPEmbeddingConfig"
+                },
+                "github_url": {
+                    "type": "string"
+                },
+                "id": {
+                    "type": "string"
+                },
+                "logo_url": {
+                    "type": "string"
+                },
+                "name": {
+                    "type": "string"
+                },
+                "name_cn": {
+                    "type": "string"
+                },
+                "openapi_config": {
+                    "$ref": "#/definitions/model.MCPOpenAPIConfig"
+                },
+                "price": {
+                    "$ref": "#/definitions/model.MCPPrice"
+                },
+                "proxy_config": {
+                    "$ref": "#/definitions/model.PublicMCPProxyConfig"
+                },
+                "readme": {
+                    "type": "string"
+                },
+                "readme_cn": {
+                    "type": "string"
+                },
+                "readme_cn_url": {
+                    "type": "string"
+                },
+                "readme_url": {
+                    "type": "string"
+                },
+                "status": {
+                    "$ref": "#/definitions/model.PublicMCPStatus"
+                },
+                "tags": {
+                    "type": "array",
+                    "items": {
+                        "type": "string"
+                    }
+                },
+                "test_config": {
+                    "$ref": "#/definitions/model.TestConfig"
+                },
                 "type": {
                     "$ref": "#/definitions/model.PublicMCPType"
                 },
@@ -8567,6 +8707,9 @@
                 "name": {
                     "type": "string"
                 },
+                "name_cn": {
+                    "type": "string"
+                },
                 "openapi_config": {
                     "$ref": "#/definitions/model.MCPOpenAPIConfig"
                 },
@@ -8597,6 +8740,9 @@
                         "type": "string"
                     }
                 },
+                "test_config": {
+                    "$ref": "#/definitions/model.TestConfig"
+                },
                 "type": {
                     "$ref": "#/definitions/model.PublicMCPType"
                 },
@@ -8939,6 +9085,78 @@
                 }
             }
         },
+        "mcp.Tool": {
+            "type": "object",
+            "properties": {
+                "annotations": {
+                    "description": "Optional properties describing tool behavior",
+                    "allOf": [
+                        {
+                            "$ref": "#/definitions/mcp.ToolAnnotation"
+                        }
+                    ]
+                },
+                "description": {
+                    "description": "A human-readable description of the tool.",
+                    "type": "string"
+                },
+                "inputSchema": {
+                    "description": "A JSON Schema object defining the expected parameters for the tool.",
+                    "allOf": [
+                        {
+                            "$ref": "#/definitions/mcp.ToolInputSchema"
+                        }
+                    ]
+                },
+                "name": {
+                    "description": "The name of the tool.",
+                    "type": "string"
+                }
+            }
+        },
+        "mcp.ToolAnnotation": {
+            "type": "object",
+            "properties": {
+                "destructiveHint": {
+                    "description": "If true, the tool may perform destructive updates",
+                    "type": "boolean"
+                },
+                "idempotentHint": {
+                    "description": "If true, repeated calls with same args have no additional effect",
+                    "type": "boolean"
+                },
+                "openWorldHint": {
+                    "description": "If true, tool interacts with external entities",
+                    "type": "boolean"
+                },
+                "readOnlyHint": {
+                    "description": "If true, the tool does not modify its environment",
+                    "type": "boolean"
+                },
+                "title": {
+                    "description": "Human-readable title for the tool",
+                    "type": "string"
+                }
+            }
+        },
+        "mcp.ToolInputSchema": {
+            "type": "object",
+            "properties": {
+                "properties": {
+                    "type": "object",
+                    "additionalProperties": {}
+                },
+                "required": {
+                    "type": "array",
+                    "items": {
+                        "type": "string"
+                    }
+                },
+                "type": {
+                    "type": "string"
+                }
+            }
+        },
         "middleware.APIResponse": {
             "type": "object",
             "properties": {
@@ -10242,6 +10460,12 @@
                 }
             }
         },
+        "model.Params": {
+            "type": "object",
+            "additionalProperties": {
+                "type": "string"
+            }
+        },
         "model.ParsePdfResponse": {
             "type": "object",
             "properties": {
@@ -10358,6 +10582,9 @@
                 "name": {
                     "type": "string"
                 },
+                "name_cn": {
+                    "type": "string"
+                },
                 "openapi_config": {
                     "$ref": "#/definitions/model.MCPOpenAPIConfig"
                 },
@@ -10388,6 +10615,9 @@
                         "type": "string"
                     }
                 },
+                "test_config": {
+                    "$ref": "#/definitions/model.TestConfig"
+                },
                 "type": {
                     "$ref": "#/definitions/model.PublicMCPType"
                 },
@@ -10452,10 +10682,7 @@
                     "type": "string"
                 },
                 "params": {
-                    "type": "object",
-                    "additionalProperties": {
-                        "type": "string"
-                    }
+                    "$ref": "#/definitions/model.Params"
                 },
                 "update_at": {
                     "type": "string"
@@ -10692,6 +10919,17 @@
                 }
             }
         },
+        "model.TestConfig": {
+            "type": "object",
+            "properties": {
+                "enabled": {
+                    "type": "boolean"
+                },
+                "params": {
+                    "$ref": "#/definitions/model.Params"
+                }
+            }
+        },
         "model.TextResponse": {
             "type": "object",
             "properties": {

+ 158 - 4
core/docs/swagger.yaml

@@ -167,6 +167,8 @@ definitions:
         type: string
       name:
         type: string
+      name_cn:
+        type: string
       readme:
         type: string
       readme_cn:
@@ -234,7 +236,7 @@ definitions:
       update_at:
         type: string
     type: object
-  controller.GroupPublicMCPResponse:
+  controller.GroupPublicMCPDetailResponse:
     properties:
       created_at:
         type: string
@@ -254,6 +256,8 @@ definitions:
         type: string
       name:
         type: string
+      name_cn:
+        type: string
       openapi_config:
         $ref: '#/definitions/model.MCPOpenAPIConfig'
       params:
@@ -282,6 +286,59 @@ definitions:
         items:
           type: string
         type: array
+      test_config:
+        $ref: '#/definitions/model.TestConfig'
+      tools:
+        items:
+          $ref: '#/definitions/mcp.Tool'
+        type: array
+      type:
+        $ref: '#/definitions/model.PublicMCPType'
+      update_at:
+        type: string
+    type: object
+  controller.GroupPublicMCPResponse:
+    properties:
+      created_at:
+        type: string
+      description:
+        type: string
+      description_cn:
+        type: string
+      embed_config:
+        $ref: '#/definitions/model.MCPEmbeddingConfig'
+      github_url:
+        type: string
+      id:
+        type: string
+      logo_url:
+        type: string
+      name:
+        type: string
+      name_cn:
+        type: string
+      openapi_config:
+        $ref: '#/definitions/model.MCPOpenAPIConfig'
+      price:
+        $ref: '#/definitions/model.MCPPrice'
+      proxy_config:
+        $ref: '#/definitions/model.PublicMCPProxyConfig'
+      readme:
+        type: string
+      readme_cn:
+        type: string
+      readme_cn_url:
+        type: string
+      readme_url:
+        type: string
+      status:
+        $ref: '#/definitions/model.PublicMCPStatus'
+      tags:
+        items:
+          type: string
+        type: array
+      test_config:
+        $ref: '#/definitions/model.TestConfig'
       type:
         $ref: '#/definitions/model.PublicMCPType'
       update_at:
@@ -394,6 +451,8 @@ definitions:
         type: string
       name:
         type: string
+      name_cn:
+        type: string
       openapi_config:
         $ref: '#/definitions/model.MCPOpenAPIConfig'
       price:
@@ -414,6 +473,8 @@ definitions:
         items:
           type: string
         type: array
+      test_config:
+        $ref: '#/definitions/model.TestConfig'
       type:
         $ref: '#/definitions/model.PublicMCPType'
       update_at:
@@ -637,6 +698,54 @@ definitions:
       web_search_count:
         type: integer
     type: object
+  mcp.Tool:
+    properties:
+      annotations:
+        allOf:
+        - $ref: '#/definitions/mcp.ToolAnnotation'
+        description: Optional properties describing tool behavior
+      description:
+        description: A human-readable description of the tool.
+        type: string
+      inputSchema:
+        allOf:
+        - $ref: '#/definitions/mcp.ToolInputSchema'
+        description: A JSON Schema object defining the expected parameters for the
+          tool.
+      name:
+        description: The name of the tool.
+        type: string
+    type: object
+  mcp.ToolAnnotation:
+    properties:
+      destructiveHint:
+        description: If true, the tool may perform destructive updates
+        type: boolean
+      idempotentHint:
+        description: If true, repeated calls with same args have no additional effect
+        type: boolean
+      openWorldHint:
+        description: If true, tool interacts with external entities
+        type: boolean
+      readOnlyHint:
+        description: If true, the tool does not modify its environment
+        type: boolean
+      title:
+        description: Human-readable title for the tool
+        type: string
+    type: object
+  mcp.ToolInputSchema:
+    properties:
+      properties:
+        additionalProperties: {}
+        type: object
+      required:
+        items:
+          type: string
+        type: array
+      type:
+        type: string
+    type: object
   middleware.APIResponse:
     properties:
       data: {}
@@ -1562,6 +1671,10 @@ definitions:
       value:
         type: string
     type: object
+  model.Params:
+    additionalProperties:
+      type: string
+    type: object
   model.ParsePdfResponse:
     properties:
       markdown:
@@ -1642,6 +1755,8 @@ definitions:
         type: string
       name:
         type: string
+      name_cn:
+        type: string
       openapi_config:
         $ref: '#/definitions/model.MCPOpenAPIConfig'
       price:
@@ -1662,6 +1777,8 @@ definitions:
         items:
           type: string
         type: array
+      test_config:
+        $ref: '#/definitions/model.TestConfig'
       type:
         $ref: '#/definitions/model.PublicMCPType'
       update_at:
@@ -1704,9 +1821,7 @@ definitions:
       mcp_id:
         type: string
       params:
-        additionalProperties:
-          type: string
-        type: object
+        $ref: '#/definitions/model.Params'
       update_at:
         type: string
     type: object
@@ -1863,6 +1978,13 @@ definitions:
       web_search_count:
         type: integer
     type: object
+  model.TestConfig:
+    properties:
+      enabled:
+        type: boolean
+      params:
+        $ref: '#/definitions/model.Params'
+    type: object
   model.TextResponse:
     properties:
       choices:
@@ -2992,6 +3114,38 @@ paths:
       tags:
       - mcp
       - group
+  /api/group/{group}/mcp/{id}:
+    get:
+      description: Get a specific MCP by its ID
+      parameters:
+      - description: Group ID
+        in: path
+        name: group
+        required: true
+        type: string
+      - description: MCP ID
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            allOf:
+            - $ref: '#/definitions/middleware.APIResponse'
+            - properties:
+                data:
+                  $ref: '#/definitions/controller.GroupPublicMCPDetailResponse'
+              type: object
+      security:
+      - ApiKeyAuth: []
+      summary: Get MCP by ID
+      tags:
+      - mcp
+      - group
   /api/group/{group}/model_config/{model}:
     delete:
       description: Delete group model config

+ 7 - 0
core/model/cache.go

@@ -643,6 +643,13 @@ func CacheSetPublicMCPReusingParam(param *PublicMCPReusingParamCache) error {
 }
 
 func CacheGetPublicMCPReusingParam(mcpID, groupID string) (*PublicMCPReusingParamCache, error) {
+	if groupID == "" {
+		return &PublicMCPReusingParamCache{
+			MCPID:   mcpID,
+			GroupID: groupID,
+			Params:  make(map[string]string),
+		}, nil
+	}
 	if !common.RedisEnabled {
 		param, err := GetPublicMCPReusingParam(mcpID, groupID)
 		if err != nil {

+ 48 - 30
core/model/publicmcp.go

@@ -65,13 +65,15 @@ type PublicMCPProxyConfig struct {
 	Reusing map[string]PublicMCPProxyReusingParam `json:"reusing"`
 }
 
+type Params = map[string]string
+
 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:"-"`
-	Params    map[string]string `gorm:"serializer:fastjson;type:text" json:"params"`
+	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:"-"`
+	Params    Params    `gorm:"serializer:fastjson;type:text" json:"params"`
 }
 
 func (p *PublicMCPReusingParam) BeforeCreate(_ *gorm.DB) (err error) {
@@ -123,27 +125,36 @@ func validateMCPID(id string) error {
 	return nil
 }
 
+type TestConfig struct {
+	Enabled bool   `json:"enabled"`
+	Params  Params `json:"params"`
+}
+
 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"`
-	Description            string                  `                                     json:"description"`
-	DescriptionCN          string                  `                                     json:"description_cn"`
-	GitHubURL              string                  `                                     json:"github_url"`
-	Readme                 string                  `gorm:"type:text"                     json:"readme"`
-	ReadmeCN               string                  `gorm:"type:text"                     json:"readme_cn"`
-	ReadmeURL              string                  `                                     json:"readme_url"`
-	ReadmeCNURL            string                  `                                     json:"readme_cn_url"`
-	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"`
+	ID                     string                  `gorm:"primaryKey"           json:"id"`
+	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"`
+	NameCN        string          `json:"name_cn,omitempty"`
+	Status        PublicMCPStatus `json:"status"                   gorm:"index;default:1"`
+	Type          PublicMCPType   `json:"type,omitempty"           gorm:"index"`
+	Description   string          `json:"description"`
+	DescriptionCN string          `json:"description_cn,omitempty"`
+	GitHubURL     string          `json:"github_url"`
+	Readme        string          `json:"readme,omitempty"         gorm:"type:text"`
+	ReadmeCN      string          `json:"readme_cn,omitempty"      gorm:"type:text"`
+	ReadmeURL     string          `json:"readme_url,omitempty"`
+	ReadmeCNURL   string          `json:"readme_cn_url,omitempty"`
+	Tags          []string        `json:"tags,omitempty"           gorm:"serializer:fastjson;type:text"`
+	LogoURL       string          `json:"logo_url,omitempty"`
+	Price         MCPPrice        `json:"price"                    gorm:"embedded"`
+
+	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"`
+	TestConfig    *TestConfig           `gorm:"serializer:fastjson;type:text" json:"test_config,omitempty"`
 }
 
 func (p *PublicMCP) BeforeCreate(_ *gorm.DB) error {
@@ -230,14 +241,18 @@ func UpdatePublicMCP(mcp *PublicMCP) (err error) {
 
 	selects := []string{
 		"github_url",
+		"description",
+		"description_cn",
 		"readme",
+		"readme_cn",
 		"readme_url",
+		"readme_cn_url",
 		"tags",
-		"author",
 		"logo_url",
 		"proxy_config",
 		"openapi_config",
 		"embed_config",
+		"test_config",
 	}
 	if mcp.Status != 0 {
 		selects = append(selects, "status")
@@ -245,6 +260,9 @@ func UpdatePublicMCP(mcp *PublicMCP) (err error) {
 	if mcp.Name != "" {
 		selects = append(selects, "name")
 	}
+	if mcp.NameCN != "" {
+		selects = append(selects, "name_cn")
+	}
 	if mcp.Type != "" {
 		selects = append(selects, "type")
 	}
@@ -443,11 +461,11 @@ func DeletePublicMCPReusingParam(mcpID, groupID string) (err error) {
 }
 
 // GetPublicMCPReusingParam retrieves a GroupMCPReusingParam by MCP ID and Group ID
-func GetPublicMCPReusingParam(mcpID, groupID string) (*PublicMCPReusingParam, error) {
+func GetPublicMCPReusingParam(mcpID, groupID string) (PublicMCPReusingParam, error) {
 	if mcpID == "" || groupID == "" {
-		return nil, errors.New("MCP ID or Group ID is empty")
+		return PublicMCPReusingParam{}, 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)
+	return param, HandleNotFound(err, ErrMCPReusingParamNotFound)
 }

+ 1 - 0
core/router/api.go

@@ -79,6 +79,7 @@ func SetAPIRouter(router *gin.Engine) {
 			groupMcpRoute := groupRoute.Group("/:group/mcp")
 			{
 				groupMcpRoute.GET("/", mcp.GetGroupPublicMCPs)
+				groupMcpRoute.GET("/:id", mcp.GetGroupPublicMCPByID)
 			}
 		}
 

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

@@ -20,6 +20,7 @@ func init() {
 			"12306",
 			"12306 Train Ticket Query",
 			model.PublicMCPTypeEmbed,
+			mcpservers.WithNameCN("12306 购票搜索"),
 			mcpservers.WithNewServerFunc(NewServer),
 			mcpservers.WithGitHubURL(
 				"https://github.com/Joooook/12306-mcp",

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

@@ -17,6 +17,7 @@ func init() {
 			"alipay",
 			"Alipay",
 			model.PublicMCPTypeDocs,
+			mcpservers.WithNameCN("支付宝"),
 			mcpservers.WithTags([]string{"pay"}),
 			mcpservers.WithDescription(
 				"支付宝 MCP Server,让你可以轻松将支付宝开放平台提供的交易创建、查询、退款等能力集成到你的 LLM 应用中,并进一步创建具备支付能力的智能工具。",

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

@@ -12,6 +12,7 @@ func init() {
 			"amap",
 			"AMAP",
 			model.PublicMCPTypeEmbed,
+			mcpservers.WithNameCN("高德地图"),
 			mcpservers.WithNewServerFunc(NewServer),
 			mcpservers.WithConfigTemplates(configTemplates),
 			mcpservers.WithTags([]string{"map"}),

+ 1 - 0
mcp-servers/baidu-map/init.go

@@ -16,6 +16,7 @@ func init() {
 			"baidu-map",
 			"Baidu Map",
 			model.PublicMCPTypeEmbed,
+			mcpservers.WithNameCN("百度地图"),
 			mcpservers.WithNewServerFunc(NewServer),
 			mcpservers.WithGitHubURL(
 				"https://github.com/baidu-maps/mcp",

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

@@ -16,6 +16,7 @@ func init() {
 			"bing-cn-search",
 			"Bing CN Search",
 			model.PublicMCPTypeEmbed,
+			mcpservers.WithNameCN("必应中国搜索"),
 			mcpservers.WithNewServerFunc(NewServer),
 			mcpservers.WithGitHubURL(
 				"https://github.com/yan5236/bing-cn-mcp-server",

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

@@ -19,6 +19,7 @@ func init() {
 			"fetch",
 			"Fetch",
 			model.PublicMCPTypeEmbed,
+			mcpservers.WithNameCN("网页内容获取"),
 			mcpservers.WithNewServerFunc(NewServer),
 			mcpservers.WithGitHubURL(
 				"https://github.com/modelcontextprotocol/servers/tree/main/src/fetch",

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

@@ -20,6 +20,7 @@ func init() {
 			"gezhe",
 			"Gezhe",
 			model.PublicMCPTypeProxyStreamable,
+			mcpservers.WithNameCN("歌者"),
 			mcpservers.WithProxyConfigType(configTemplates),
 			mcpservers.WithTags([]string{"map"}),
 			mcpservers.WithDescription(

+ 1 - 0
mcp-servers/gpt-vis/init.go

@@ -19,6 +19,7 @@ func init() {
 			"gpt-vis",
 			"GPT Vis",
 			model.PublicMCPTypeEmbed,
+			mcpservers.WithNameCN("可视化图表"),
 			mcpservers.WithNewServerFunc(NewServer),
 			mcpservers.WithGitHubURL(
 				"https://github.com/antvis/mcp-server-chart",

+ 1 - 0
mcp-servers/hefeng-weather/init.go

@@ -19,6 +19,7 @@ func init() {
 			"hefeng-weather",
 			"HeFeng Weather",
 			model.PublicMCPTypeEmbed,
+			mcpservers.WithNameCN("和风天气"),
 			mcpservers.WithNewServerFunc(NewServer),
 			mcpservers.WithGitHubURL(
 				"https://github.com/shanggqm/hefeng-mcp-weather",

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

@@ -19,6 +19,7 @@ func init() {
 			"howtocook",
 			"HowToCook Recipe Server",
 			model.PublicMCPTypeEmbed,
+			mcpservers.WithNameCN("程序员做饭指南"),
 			mcpservers.WithNewServerFunc(NewServer),
 			mcpservers.WithDescription(
 				"A recipe recommendation server based on the HowToCook project. Provides intelligent meal planning, recipe search by category, and dish recommendations based on the number of people.",

+ 1 - 0
mcp-servers/jina-tools/init.go

@@ -19,6 +19,7 @@ func init() {
 			"jina",
 			"Jina AI Tools",
 			model.PublicMCPTypeEmbed,
+			mcpservers.WithNameCN("Jina AI 工具"),
 			mcpservers.WithNewServerFunc(NewServer),
 			mcpservers.WithGitHubURL(
 				"https://github.com/PsychArch/jina-mcp-tools",

+ 7 - 1
mcp-servers/mcp.go

@@ -138,6 +138,12 @@ type McpServer struct {
 
 type McpConfig func(*McpServer)
 
+func WithNameCN(nameCN string) McpConfig {
+	return func(e *McpServer) {
+		e.NameCN = nameCN
+	}
+}
+
 func WithDescription(description string) McpConfig {
 	return func(e *McpServer) {
 		e.Description = description
@@ -237,7 +243,7 @@ func NewMcp(id, name string, mcpType model.PublicMCPType, opts ...McpConfig) Mcp
 }
 
 func (e *McpServer) NewServer(config, reusingConfig map[string]string) (Server, error) {
-	if e.newServer != nil {
+	if e.newServer == nil {
 		return nil, errors.New("not impl new server")
 	}
 	if err := ValidateConfigTemplatesConfig(e.ConfigTemplates, config, reusingConfig); err != nil {

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

@@ -19,6 +19,7 @@ func init() {
 			"tavily",
 			"Tavily AI Search",
 			model.PublicMCPTypeEmbed,
+			mcpservers.WithNameCN("Tavily AI 搜索"),
 			mcpservers.WithNewServerFunc(NewServer),
 			mcpservers.WithDescription(
 				"A powerful web search MCP server powered by Tavily's AI search engine. Provides real-time web search, content extraction, web crawling, and site mapping capabilities with advanced filtering and customization options.",

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

@@ -19,6 +19,7 @@ func init() {
 			"time",
 			"Time",
 			model.PublicMCPTypeEmbed,
+			mcpservers.WithNameCN("时间"),
 			mcpservers.WithNewServerFunc(NewServer),
 			mcpservers.WithGitHubURL(
 				"https://github.com/modelcontextprotocol/servers/tree/main/src/time",

+ 1 - 0
mcp-servers/web-search/init.go

@@ -20,6 +20,7 @@ func init() {
 			"web-search",
 			"Web Search",
 			model.PublicMCPTypeEmbed,
+			mcpservers.WithNameCN("网络搜索"),
 			mcpservers.WithNewServerFunc(NewServer),
 			mcpservers.WithConfigTemplates(configTemplates),
 			mcpservers.WithTags([]string{"search", "web", "google", "bing", "arxiv", "searchxng"}),