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

feat: group custom mcp support (#141)

* feat: group mcp support

* fix: group mcp route

* chore: bumpopenapi mcp go mod

* feat: impl openapi server addr and auth
zijiren 8 месяцев назад
Родитель
Сommit
53358483f5

+ 15 - 0
core/common/utils.go

@@ -0,0 +1,15 @@
+package common
+
+import (
+	"encoding/hex"
+
+	"github.com/google/uuid"
+	"github.com/labring/aiproxy/core/common/conv"
+)
+
+func ShortUUID() string {
+	var buf [32]byte
+	bytes := uuid.New()
+	hex.Encode(buf[:], bytes[:])
+	return conv.BytesToString(buf[:])
+}

+ 165 - 0
core/controller/groupmcp-server.go

@@ -0,0 +1,165 @@
+package controller
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"net/url"
+
+	"github.com/gin-gonic/gin"
+	"github.com/labring/aiproxy/core/common"
+	"github.com/labring/aiproxy/core/common/mcpproxy"
+	"github.com/labring/aiproxy/core/middleware"
+	"github.com/labring/aiproxy/core/model"
+	"github.com/mark3labs/mcp-go/server"
+)
+
+type groupMcpEndpointProvider struct {
+	key string
+	t   model.GroupMCPType
+}
+
+func newGroupMcpEndpoint(key string, t model.GroupMCPType) mcpproxy.EndpointProvider {
+	return &groupMcpEndpointProvider{
+		key: key,
+		t:   t,
+	}
+}
+
+func (m *groupMcpEndpointProvider) NewEndpoint() (newSession string, newEndpoint string) {
+	session := common.ShortUUID()
+	endpoint := fmt.Sprintf("/mcp/group/message?sessionId=%s&key=%s&type=%s", session, m.key, m.t)
+	return session, endpoint
+}
+
+func (m *groupMcpEndpointProvider) LoadEndpoint(endpoint string) (session string) {
+	parsedURL, err := url.Parse(endpoint)
+	if err != nil {
+		return ""
+	}
+	return parsedURL.Query().Get("sessionId")
+}
+
+// GroupMCPSseServer godoc
+//
+//	@Summary	Group MCP SSE Server
+//	@Router		/mcp/group/{id}/sse [get]
+func GroupMCPSseServer(c *gin.Context) {
+	id := c.Param("id")
+	if id == "" {
+		middleware.ErrorResponse(c, http.StatusBadRequest, "MCP ID, Group ID, and Session are required")
+		return
+	}
+
+	group := middleware.GetGroup(c)
+
+	mcp, err := model.GetGroupMCPByID(id, group.ID)
+	if err != nil {
+		middleware.ErrorResponse(c, http.StatusNotFound, err.Error())
+		return
+	}
+
+	switch mcp.Type {
+	case model.GroupMCPTypeProxySSE:
+		handleGroupProxySSE(c, mcp.ProxySSEConfig)
+	case model.GroupMCPTypeOpenAPI:
+		server, err := newOpenAPIMCPServer(mcp.OpenAPIConfig)
+		if err != nil {
+			middleware.AbortLogWithMessage(c, http.StatusBadRequest, err.Error())
+			return
+		}
+		handleGroupMCPServer(c, server, model.GroupMCPTypeOpenAPI)
+	default:
+		middleware.ErrorResponse(c, http.StatusBadRequest, "Unsupported MCP type")
+	}
+}
+
+// handlePublicProxySSE processes SSE proxy requests
+func handleGroupProxySSE(c *gin.Context, config *model.GroupMCPProxySSEConfig) {
+	if config == nil || config.URL == "" {
+		return
+	}
+
+	backendURL, err := url.Parse(config.URL)
+	if err != nil {
+		middleware.AbortLogWithMessage(c, http.StatusBadRequest, err.Error())
+		return
+	}
+
+	headers := make(map[string]string)
+	backendQuery := &url.Values{}
+	token := middleware.GetToken(c)
+
+	for k, v := range config.Headers {
+		headers[k] = v
+	}
+	for k, v := range config.Querys {
+		backendQuery.Set(k, v)
+	}
+
+	backendURL.RawQuery = backendQuery.Encode()
+	mcpproxy.SSEHandler(
+		c.Writer,
+		c.Request,
+		getStore(),
+		newPublicMcpEndpoint(token.Key, model.PublicMCPTypeProxySSE),
+		backendURL.String(),
+		headers,
+	)
+}
+
+// handleMCPServer handles the SSE connection for an MCP server
+func handleGroupMCPServer(c *gin.Context, s *server.MCPServer, mcpType model.GroupMCPType) {
+	token := middleware.GetToken(c)
+
+	newSession, newEndpoint := newGroupMcpEndpoint(token.Key, mcpType).NewEndpoint()
+	server := NewSSEServer(
+		s,
+		WithMessageEndpoint(newEndpoint),
+	)
+
+	// Store the session
+	store := getStore()
+	store.Set(newSession, string(mcpType))
+	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.HandleSSE(c.Writer, c.Request)
+}
+
+// GroupMCPMessage godoc
+//
+//	@Summary	MCP SSE Proxy
+//	@Router		/mcp/group/message [post]
+func GroupMCPMessage(c *gin.Context) {
+	token := middleware.GetToken(c)
+	mcpTypeStr, _ := c.GetQuery("type")
+	if mcpTypeStr == "" {
+		return
+	}
+	mcpType := model.GroupMCPType(mcpTypeStr)
+	sessionID, _ := c.GetQuery("sessionId")
+	if sessionID == "" {
+		return
+	}
+
+	switch mcpType {
+	case model.GroupMCPTypeProxySSE:
+		mcpproxy.ProxyHandler(
+			c.Writer,
+			c.Request,
+			getStore(),
+			newGroupMcpEndpoint(token.Key, mcpType),
+		)
+	default:
+		sendMCPSSEMessage(c, mcpTypeStr, sessionID)
+	}
+}

+ 177 - 0
core/controller/groupmcp.go

@@ -0,0 +1,177 @@
+package controller
+
+import (
+	"net/http"
+
+	"github.com/gin-gonic/gin"
+	"github.com/labring/aiproxy/core/middleware"
+	"github.com/labring/aiproxy/core/model"
+)
+
+// GetGroupMCPs godoc
+//
+//	@Summary		Get Group MCPs
+//	@Description	Get a list of Group MCPs with pagination and filtering
+//	@Tags			mcp
+//	@Produce		json
+//	@Security		ApiKeyAuth
+//	@Param			group		path		string	true	"Group ID"
+//	@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"
+//	@Success		200			{object}	middleware.APIResponse{data=[]model.GroupMCP}
+//	@Router			/api/mcp/group/{group} [get]
+func GetGroupMCPs(c *gin.Context) {
+	groupID := c.Param("group")
+	if groupID == "" {
+		middleware.ErrorResponse(c, http.StatusBadRequest, "Group ID is required")
+		return
+	}
+
+	page, perPage := parsePageParams(c)
+	mcpType := model.PublicMCPType(c.Query("type"))
+	keyword := c.Query("keyword")
+
+	mcps, total, err := model.GetGroupMCPs(groupID, page, perPage, mcpType, keyword)
+	if err != nil {
+		middleware.ErrorResponse(c, http.StatusInternalServerError, err.Error())
+		return
+	}
+
+	middleware.SuccessResponse(c, gin.H{
+		"mcps":  mcps,
+		"total": total,
+	})
+}
+
+// GetGroupMCPByID godoc
+//
+//	@Summary		Get Group MCP by ID
+//	@Description	Get a specific Group MCP by its ID and Group ID
+//	@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.GroupMCP}
+//	@Router			/api/mcp/group/{group}/{id} [get]
+func GetGroupMCPByID(c *gin.Context) {
+	id := c.Param("id")
+	groupID := c.Param("group")
+
+	if id == "" || groupID == "" {
+		middleware.ErrorResponse(c, http.StatusBadRequest, "MCP ID and Group ID are required")
+		return
+	}
+
+	mcp, err := model.GetGroupMCPByID(id, groupID)
+	if err != nil {
+		middleware.ErrorResponse(c, http.StatusNotFound, err.Error())
+		return
+	}
+
+	middleware.SuccessResponse(c, mcp)
+}
+
+// CreateGroupMCP godoc
+//
+//	@Summary		Create Group MCP
+//	@Description	Create a new Group MCP
+//	@Tags			mcp
+//	@Accept			json
+//	@Produce		json
+//	@Security		ApiKeyAuth
+//	@Param			group	path		string			true	"Group ID"
+//	@Param			mcp		body		model.GroupMCP	true	"Group MCP object"
+//	@Success		200		{object}	middleware.APIResponse{data=model.GroupMCP}
+//	@Router			/api/mcp/group/{group} [post]
+func CreateGroupMCP(c *gin.Context) {
+	groupID := c.Param("group")
+	if groupID == "" {
+		middleware.ErrorResponse(c, http.StatusBadRequest, "Group ID is required")
+		return
+	}
+
+	var mcp model.GroupMCP
+	if err := c.ShouldBindJSON(&mcp); err != nil {
+		middleware.ErrorResponse(c, http.StatusBadRequest, err.Error())
+		return
+	}
+
+	mcp.GroupID = groupID
+
+	if err := model.CreateGroupMCP(&mcp); err != nil {
+		middleware.ErrorResponse(c, http.StatusInternalServerError, err.Error())
+		return
+	}
+
+	middleware.SuccessResponse(c, mcp)
+}
+
+// UpdateGroupMCP godoc
+//
+//	@Summary		Update Group MCP
+//	@Description	Update an existing Group MCP
+//	@Tags			mcp
+//	@Accept			json
+//	@Produce		json
+//	@Security		ApiKeyAuth
+//	@Param			id		path		string			true	"MCP ID"
+//	@Param			group	path		string			true	"Group ID"
+//	@Param			mcp		body		model.GroupMCP	true	"Group MCP object"
+//	@Success		200		{object}	middleware.APIResponse{data=model.GroupMCP}
+//	@Router			/api/mcp/group/{group}/{id} [put]
+func UpdateGroupMCP(c *gin.Context) {
+	id := c.Param("id")
+	groupID := c.Param("group")
+
+	if id == "" || groupID == "" {
+		middleware.ErrorResponse(c, http.StatusBadRequest, "MCP ID and Group ID are required")
+		return
+	}
+
+	var mcp model.GroupMCP
+	if err := c.ShouldBindJSON(&mcp); err != nil {
+		middleware.ErrorResponse(c, http.StatusBadRequest, err.Error())
+		return
+	}
+
+	mcp.ID = id
+	mcp.GroupID = groupID
+
+	if err := model.UpdateGroupMCP(&mcp); err != nil {
+		middleware.ErrorResponse(c, http.StatusInternalServerError, err.Error())
+		return
+	}
+
+	middleware.SuccessResponse(c, mcp)
+}
+
+// DeleteGroupMCP godoc
+//
+//	@Summary		Delete Group MCP
+//	@Description	Delete a Group MCP by ID and Group ID
+//	@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
+//	@Router			/api/mcp/group/{group}/{id} [delete]
+func DeleteGroupMCP(c *gin.Context) {
+	id := c.Param("id")
+	groupID := c.Param("group")
+
+	if id == "" || groupID == "" {
+		middleware.ErrorResponse(c, http.StatusBadRequest, "MCP ID and Group ID are required")
+		return
+	}
+
+	if err := model.DeleteGroupMCP(id, groupID); err != nil {
+		middleware.ErrorResponse(c, http.StatusInternalServerError, err.Error())
+		return
+	}
+
+	middleware.SuccessResponse(c, nil)
+}

+ 104 - 81
core/controller/mcpproxy.go → core/controller/publicmcp-server.go

@@ -12,35 +12,35 @@ import (
 	"time"
 
 	"github.com/gin-gonic/gin"
-	"github.com/google/uuid"
 	"github.com/labring/aiproxy/core/common"
 	"github.com/labring/aiproxy/core/common/mcpproxy"
 	"github.com/labring/aiproxy/core/middleware"
 	"github.com/labring/aiproxy/core/model"
 	"github.com/labring/aiproxy/openapi-mcp/convert"
+	"github.com/mark3labs/mcp-go/server"
 	"github.com/redis/go-redis/v9"
 )
 
-// mcpEndpointProvider implements the EndpointProvider interface for MCP
-type mcpEndpointProvider struct {
+// publicMcpEndpointProvider implements the EndpointProvider interface for MCP
+type publicMcpEndpointProvider struct {
 	key string
 	t   model.PublicMCPType
 }
 
-func newEndpoint(key string, t model.PublicMCPType) mcpproxy.EndpointProvider {
-	return &mcpEndpointProvider{
+func newPublicMcpEndpoint(key string, t model.PublicMCPType) mcpproxy.EndpointProvider {
+	return &publicMcpEndpointProvider{
 		key: key,
 		t:   t,
 	}
 }
 
-func (m *mcpEndpointProvider) NewEndpoint() (newSession string, newEndpoint string) {
-	session := uuid.NewString()
-	endpoint := fmt.Sprintf("/mcp/message?sessionId=%s&key=%s&type=%s", session, m.key, m.t)
+func (m *publicMcpEndpointProvider) NewEndpoint() (newSession string, newEndpoint string) {
+	session := common.ShortUUID()
+	endpoint := fmt.Sprintf("/mcp/public/message?sessionId=%s&key=%s&type=%s", session, m.key, m.t)
 	return session, endpoint
 }
 
-func (m *mcpEndpointProvider) LoadEndpoint(endpoint string) (session string) {
+func (m *publicMcpEndpointProvider) LoadEndpoint(endpoint string) (session string) {
 	parsedURL, err := url.Parse(endpoint)
 	if err != nil {
 		return ""
@@ -107,11 +107,11 @@ func (r *redisStoreManager) Delete(session string) {
 	r.rdb.Del(ctx, "mcp:session:"+session)
 }
 
-// MCPSseProxy godoc
+// PublicMCPSseServer godoc
 //
-//	@Summary	MCP SSE Proxy
+//	@Summary	Public MCP SSE Server
 //	@Router		/mcp/public/{id}/sse [get]
-func MCPSseProxy(c *gin.Context) {
+func PublicMCPSseServer(c *gin.Context) {
 	mcpID := c.Param("id")
 
 	publicMcp, err := model.GetPublicMCPByID(mcpID)
@@ -122,18 +122,22 @@ func MCPSseProxy(c *gin.Context) {
 
 	switch publicMcp.Type {
 	case model.PublicMCPTypeProxySSE:
-		handleProxySSE(c, publicMcp)
+		handlePublicProxySSE(c, publicMcp.ID, publicMcp.ProxySSEConfig)
 	case model.PublicMCPTypeOpenAPI:
-		handleOpenAPI(c, publicMcp)
+		server, err := newOpenAPIMCPServer(publicMcp.OpenAPIConfig)
+		if err != nil {
+			middleware.AbortLogWithMessage(c, http.StatusBadRequest, err.Error())
+			return
+		}
+		handleMCPServer(c, server, model.PublicMCPTypeOpenAPI)
 	default:
 		middleware.AbortLogWithMessage(c, http.StatusBadRequest, "unknow mcp type")
 		return
 	}
 }
 
-// handleProxySSE processes SSE proxy requests
-func handleProxySSE(c *gin.Context, publicMcp *model.PublicMCP) {
-	config := publicMcp.ProxySSEConfig
+// handlePublicProxySSE processes SSE proxy requests
+func handlePublicProxySSE(c *gin.Context, mcpID string, config *model.PublicMCPProxySSEConfig) {
 	if config == nil || config.URL == "" {
 		return
 	}
@@ -150,27 +154,33 @@ func handleProxySSE(c *gin.Context, publicMcp *model.PublicMCP) {
 	token := middleware.GetToken(c)
 
 	// Process reusing parameters if any
-	if err := processReusingParams(config.ReusingParams, publicMcp.ID, group.ID, headers, backendQuery); err != nil {
+	if err := processReusingParams(config.ReusingParams, mcpID, group.ID, headers, backendQuery); err != nil {
 		middleware.AbortLogWithMessage(c, http.StatusBadRequest, err.Error())
 		return
 	}
 
+	for k, v := range config.Headers {
+		headers[k] = v
+	}
+	for k, v := range config.Querys {
+		backendQuery.Set(k, v)
+	}
+
 	backendURL.RawQuery = backendQuery.Encode()
 	mcpproxy.SSEHandler(
 		c.Writer,
 		c.Request,
 		getStore(),
-		newEndpoint(token.Key, publicMcp.Type),
+		newPublicMcpEndpoint(token.Key, model.PublicMCPTypeProxySSE),
 		backendURL.String(),
 		headers,
 	)
 }
 
-// handleOpenAPI processes OpenAPI requests
-func handleOpenAPI(c *gin.Context, publicMcp *model.PublicMCP) {
-	config := publicMcp.OpenAPIConfig
+// newOpenAPIMCPServer creates a new MCP server from OpenAPI configuration
+func newOpenAPIMCPServer(config *model.MCPOpenAPIConfig) (*server.MCPServer, error) {
 	if config == nil || (config.OpenAPISpec == "" && config.OpenAPIContent == "") {
-		return
+		return nil, errors.New("invalid OpenAPI configuration")
 	}
 
 	// Parse OpenAPI specification
@@ -185,38 +195,45 @@ func handleOpenAPI(c *gin.Context, publicMcp *model.PublicMCP) {
 	}
 
 	if err != nil {
-		return
+		return nil, err
 	}
 
 	// Convert to MCP server
 	converter := convert.NewConverter(parser, convert.Options{
-		OpenAPIFrom: openAPIFrom,
+		OpenAPIFrom:   openAPIFrom,
+		ServerAddr:    config.ServerAddr,
+		Authorization: config.Authorization,
 	})
 	s, err := converter.Convert()
 	if err != nil {
-		return
+		return nil, err
 	}
 
-	token := middleware.GetToken(c)
+	return s, nil
+}
 
-	// Setup SSE server
-	newSession, newEndpoint := newEndpoint(token.Key, publicMcp.Type).NewEndpoint()
-	store := getStore()
-	store.Set(newSession, "openapi")
-	defer func() {
-		store.Delete(newSession)
-	}()
+// handleMCPServer handles the SSE connection for an MCP server
+func handleMCPServer(c *gin.Context, s *server.MCPServer, mcpType model.PublicMCPType) {
+	token := middleware.GetToken(c)
 
+	newSession, newEndpoint := newPublicMcpEndpoint(token.Key, mcpType).NewEndpoint()
 	server := NewSSEServer(
 		s,
 		WithMessageEndpoint(newEndpoint),
 	)
 
+	// Store the session
+	store := getStore()
+	store.Set(newSession, string(mcpType))
+	defer func() {
+		store.Delete(newSession)
+	}()
+
 	ctx, cancel := context.WithCancel(c.Request.Context())
 	defer cancel()
 
 	// Start message processing goroutine
-	go processOpenAPIMessages(ctx, newSession, server)
+	go processMCPSseMpscMessages(ctx, newSession, server)
 
 	// Handle SSE connection
 	server.HandleSSE(c.Writer, c.Request)
@@ -247,9 +264,9 @@ func parseOpenAPIFromContent(config *model.MCPOpenAPIConfig, parser *convert.Par
 	return parser.Parse([]byte(config.OpenAPIContent))
 }
 
-// processOpenAPIMessages handles message processing for OpenAPI
-func processOpenAPIMessages(ctx context.Context, sessionID string, server *SSEServer) {
-	mpscInstance := getMpsc()
+// processMCPSseMpscMessages handles message processing for OpenAPI
+func processMCPSseMpscMessages(ctx context.Context, sessionID string, server *SSEServer) {
+	mpscInstance := getMCPMpsc()
 	for {
 		select {
 		case <-ctx.Done():
@@ -299,11 +316,11 @@ func processReusingParams(reusingParams map[string]model.ReusingParam, mcpID str
 	return nil
 }
 
-// MCPMessage godoc
+// PublicMCPMessage godoc
 //
 //	@Summary	MCP SSE Proxy
-//	@Router		/mcp/message [post]
-func MCPMessage(c *gin.Context) {
+//	@Router		/mcp/public/message [post]
+func PublicMCPMessage(c *gin.Context) {
 	token := middleware.GetToken(c)
 	mcpTypeStr, _ := c.GetQuery("type")
 	if mcpTypeStr == "" {
@@ -321,26 +338,30 @@ func MCPMessage(c *gin.Context) {
 			c.Writer,
 			c.Request,
 			getStore(),
-			newEndpoint(token.Key, mcpType),
+			newPublicMcpEndpoint(token.Key, mcpType),
 		)
-	case model.PublicMCPTypeOpenAPI:
-		backend, ok := getStore().Get(sessionID)
-		if !ok || backend != "openapi" {
-			return
-		}
-		mpscInstance := getMpsc()
-		body, err := io.ReadAll(c.Request.Body)
-		if err != nil {
-			_ = c.AbortWithError(http.StatusInternalServerError, err)
-			return
-		}
-		err = mpscInstance.send(c.Request.Context(), sessionID, body)
-		if err != nil {
-			_ = c.AbortWithError(http.StatusInternalServerError, err)
-			return
-		}
-		c.Writer.WriteHeader(http.StatusAccepted)
+	default:
+		sendMCPSSEMessage(c, mcpTypeStr, sessionID)
+	}
+}
+
+func sendMCPSSEMessage(c *gin.Context, mcpType, sessionID string) {
+	backend, ok := getStore().Get(sessionID)
+	if !ok || backend != mcpType {
+		return
+	}
+	mpscInstance := getMCPMpsc()
+	body, err := io.ReadAll(c.Request.Body)
+	if err != nil {
+		_ = c.AbortWithError(http.StatusInternalServerError, err)
+		return
+	}
+	err = mpscInstance.send(c.Request.Context(), sessionID, body)
+	if err != nil {
+		_ = c.AbortWithError(http.StatusInternalServerError, err)
+		return
 	}
+	c.Writer.WriteHeader(http.StatusAccepted)
 }
 
 // Interface for multi-producer, single-consumer message passing
@@ -351,31 +372,31 @@ type mpsc interface {
 
 // Global MPSC instances
 var (
-	memMpsc       mpsc = newChannelMpsc()
-	redisMpsc     mpsc
-	redisMpscOnce = &sync.Once{}
+	memMCPMpsc       mpsc = newChannelMCPMpsc()
+	redisMCPMpsc     mpsc
+	redisMCPMpscOnce = &sync.Once{}
 )
 
-func getMpsc() mpsc {
+func getMCPMpsc() mpsc {
 	if common.RedisEnabled {
-		redisMpscOnce.Do(func() {
-			redisMpsc = newRedisMPSC(common.RDB)
+		redisMCPMpscOnce.Do(func() {
+			redisMCPMpsc = newRedisMCPMPSC(common.RDB)
 		})
-		return redisMpsc
+		return redisMCPMpsc
 	}
-	return memMpsc
+	return memMCPMpsc
 }
 
 // In-memory channel-based MPSC implementation
-type channelMpsc struct {
+type channelMCPMpsc struct {
 	channels     map[string]chan []byte
 	lastAccess   map[string]time.Time
 	channelMutex sync.RWMutex
 }
 
-// newChannelMpsc creates a new channel-based mpsc implementation
-func newChannelMpsc() *channelMpsc {
-	c := &channelMpsc{
+// newChannelMCPMpsc creates a new channel-based mpsc implementation
+func newChannelMCPMpsc() *channelMCPMpsc {
+	c := &channelMCPMpsc{
 		channels:   make(map[string]chan []byte),
 		lastAccess: make(map[string]time.Time),
 	}
@@ -387,7 +408,7 @@ func newChannelMpsc() *channelMpsc {
 }
 
 // cleanupExpiredChannels periodically checks for and removes channels that haven't been accessed in 5 minutes
-func (c *channelMpsc) cleanupExpiredChannels() {
+func (c *channelMCPMpsc) cleanupExpiredChannels() {
 	ticker := time.NewTicker(1 * time.Minute)
 	defer ticker.Stop()
 
@@ -409,7 +430,7 @@ func (c *channelMpsc) cleanupExpiredChannels() {
 }
 
 // getOrCreateChannel gets an existing channel or creates a new one for the session
-func (c *channelMpsc) getOrCreateChannel(id string) chan []byte {
+func (c *channelMCPMpsc) getOrCreateChannel(id string) chan []byte {
 	c.channelMutex.RLock()
 	ch, exists := c.channels[id]
 	c.channelMutex.RUnlock()
@@ -432,7 +453,7 @@ func (c *channelMpsc) getOrCreateChannel(id string) chan []byte {
 }
 
 // recv receives data for the specified session
-func (c *channelMpsc) recv(ctx context.Context, id string) ([]byte, error) {
+func (c *channelMCPMpsc) recv(ctx context.Context, id string) ([]byte, error) {
 	ch := c.getOrCreateChannel(id)
 
 	select {
@@ -447,7 +468,7 @@ func (c *channelMpsc) recv(ctx context.Context, id string) ([]byte, error) {
 }
 
 // send sends data to the specified session
-func (c *channelMpsc) send(ctx context.Context, id string, data []byte) error {
+func (c *channelMCPMpsc) send(ctx context.Context, id string, data []byte) error {
 	ch := c.getOrCreateChannel(id)
 
 	select {
@@ -461,17 +482,18 @@ func (c *channelMpsc) send(ctx context.Context, id string, data []byte) error {
 }
 
 // Redis-based MPSC implementation
-type redisMPSC struct {
+type redisMCPMPSC struct {
 	rdb *redis.Client
 }
 
-// newRedisMPSC creates a new Redis MPSC instance
-func newRedisMPSC(rdb *redis.Client) *redisMPSC {
-	return &redisMPSC{rdb: rdb}
+// newRedisMCPMPSC creates a new Redis MPSC instance
+func newRedisMCPMPSC(rdb *redis.Client) *redisMCPMPSC {
+	return &redisMCPMPSC{rdb: rdb}
 }
 
-func (r *redisMPSC) send(ctx context.Context, id string, data []byte) error {
+func (r *redisMCPMPSC) send(ctx context.Context, id string, data []byte) error {
 	// Set expiration to 5 minutes when sending data
+	id = "mcp:mpsc:" + id
 	pipe := r.rdb.Pipeline()
 	pipe.LPush(ctx, id, data)
 	pipe.Expire(ctx, id, 5*time.Minute)
@@ -479,7 +501,8 @@ func (r *redisMPSC) send(ctx context.Context, id string, data []byte) error {
 	return err
 }
 
-func (r *redisMPSC) recv(ctx context.Context, id string) ([]byte, error) {
+func (r *redisMCPMPSC) recv(ctx context.Context, id string) ([]byte, error) {
+	id = "mcp:mpsc:" + id
 	for {
 		select {
 		case <-ctx.Done():

+ 5 - 5
core/controller/publicmcp.go

@@ -164,7 +164,7 @@ func DeletePublicMCP(c *gin.Context) {
 //	@Security		ApiKeyAuth
 //	@Param			id		path		string	true	"MCP ID"
 //	@Param			group	path		string	true	"Group ID"
-//	@Success		200		{object}	middleware.APIResponse{data=model.GroupPublicMCPReusingParam}
+//	@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")
@@ -192,9 +192,9 @@ func GetGroupPublicMCPReusingParam(c *gin.Context) {
 //	@Accept			json
 //	@Produce		json
 //	@Security		ApiKeyAuth
-//	@Param			id		path		string								true	"MCP ID"
-//	@Param			group	path		string								true	"Group ID"
-//	@Param			params	body		model.GroupPublicMCPReusingParam	true	"Reusing parameters"
+//	@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) {
@@ -206,7 +206,7 @@ func SaveGroupPublicMCPReusingParam(c *gin.Context) {
 		return
 	}
 
-	var param model.GroupPublicMCPReusingParam
+	var param model.PublicMCPReusingParam
 	if err := c.ShouldBindJSON(&param); err != nil {
 		middleware.ErrorResponse(c, http.StatusBadRequest, err.Error())
 		return

+ 388 - 29
core/docs/docs.go

@@ -3213,6 +3213,290 @@ const docTemplate = `{
                 }
             }
         },
+        "/api/mcp/group/{group}": {
+            "get": {
+                "security": [
+                    {
+                        "ApiKeyAuth": []
+                    }
+                ],
+                "description": "Get a list of Group MCPs with pagination and filtering",
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "mcp"
+                ],
+                "summary": "Get Group MCPs",
+                "parameters": [
+                    {
+                        "type": "string",
+                        "description": "Group ID",
+                        "name": "group",
+                        "in": "path",
+                        "required": true
+                    },
+                    {
+                        "type": "integer",
+                        "description": "Page number",
+                        "name": "page",
+                        "in": "query"
+                    },
+                    {
+                        "type": "integer",
+                        "description": "Items per page",
+                        "name": "per_page",
+                        "in": "query"
+                    },
+                    {
+                        "type": "string",
+                        "description": "MCP type",
+                        "name": "type",
+                        "in": "query"
+                    },
+                    {
+                        "type": "string",
+                        "description": "Search keyword",
+                        "name": "keyword",
+                        "in": "query"
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "allOf": [
+                                {
+                                    "$ref": "#/definitions/middleware.APIResponse"
+                                },
+                                {
+                                    "type": "object",
+                                    "properties": {
+                                        "data": {
+                                            "type": "array",
+                                            "items": {
+                                                "$ref": "#/definitions/model.GroupMCP"
+                                            }
+                                        }
+                                    }
+                                }
+                            ]
+                        }
+                    }
+                }
+            },
+            "post": {
+                "security": [
+                    {
+                        "ApiKeyAuth": []
+                    }
+                ],
+                "description": "Create a new Group MCP",
+                "consumes": [
+                    "application/json"
+                ],
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "mcp"
+                ],
+                "summary": "Create Group MCP",
+                "parameters": [
+                    {
+                        "type": "string",
+                        "description": "Group ID",
+                        "name": "group",
+                        "in": "path",
+                        "required": true
+                    },
+                    {
+                        "description": "Group MCP object",
+                        "name": "mcp",
+                        "in": "body",
+                        "required": true,
+                        "schema": {
+                            "$ref": "#/definitions/model.GroupMCP"
+                        }
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "allOf": [
+                                {
+                                    "$ref": "#/definitions/middleware.APIResponse"
+                                },
+                                {
+                                    "type": "object",
+                                    "properties": {
+                                        "data": {
+                                            "$ref": "#/definitions/model.GroupMCP"
+                                        }
+                                    }
+                                }
+                            ]
+                        }
+                    }
+                }
+            }
+        },
+        "/api/mcp/group/{group}/{id}": {
+            "get": {
+                "security": [
+                    {
+                        "ApiKeyAuth": []
+                    }
+                ],
+                "description": "Get a specific Group MCP by its ID and Group ID",
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "mcp"
+                ],
+                "summary": "Get Group MCP by ID",
+                "parameters": [
+                    {
+                        "type": "string",
+                        "description": "MCP ID",
+                        "name": "id",
+                        "in": "path",
+                        "required": true
+                    },
+                    {
+                        "type": "string",
+                        "description": "Group ID",
+                        "name": "group",
+                        "in": "path",
+                        "required": true
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "allOf": [
+                                {
+                                    "$ref": "#/definitions/middleware.APIResponse"
+                                },
+                                {
+                                    "type": "object",
+                                    "properties": {
+                                        "data": {
+                                            "$ref": "#/definitions/model.GroupMCP"
+                                        }
+                                    }
+                                }
+                            ]
+                        }
+                    }
+                }
+            },
+            "put": {
+                "security": [
+                    {
+                        "ApiKeyAuth": []
+                    }
+                ],
+                "description": "Update an existing Group MCP",
+                "consumes": [
+                    "application/json"
+                ],
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "mcp"
+                ],
+                "summary": "Update Group MCP",
+                "parameters": [
+                    {
+                        "type": "string",
+                        "description": "MCP ID",
+                        "name": "id",
+                        "in": "path",
+                        "required": true
+                    },
+                    {
+                        "type": "string",
+                        "description": "Group ID",
+                        "name": "group",
+                        "in": "path",
+                        "required": true
+                    },
+                    {
+                        "description": "Group MCP object",
+                        "name": "mcp",
+                        "in": "body",
+                        "required": true,
+                        "schema": {
+                            "$ref": "#/definitions/model.GroupMCP"
+                        }
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "allOf": [
+                                {
+                                    "$ref": "#/definitions/middleware.APIResponse"
+                                },
+                                {
+                                    "type": "object",
+                                    "properties": {
+                                        "data": {
+                                            "$ref": "#/definitions/model.GroupMCP"
+                                        }
+                                    }
+                                }
+                            ]
+                        }
+                    }
+                }
+            },
+            "delete": {
+                "security": [
+                    {
+                        "ApiKeyAuth": []
+                    }
+                ],
+                "description": "Delete a Group MCP by ID and Group ID",
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "mcp"
+                ],
+                "summary": "Delete Group MCP",
+                "parameters": [
+                    {
+                        "type": "string",
+                        "description": "MCP ID",
+                        "name": "id",
+                        "in": "path",
+                        "required": true
+                    },
+                    {
+                        "type": "string",
+                        "description": "Group ID",
+                        "name": "group",
+                        "in": "path",
+                        "required": true
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "$ref": "#/definitions/middleware.APIResponse"
+                        }
+                    }
+                }
+            }
+        },
         "/api/mcp/public/": {
             "get": {
                 "security": [
@@ -3481,7 +3765,7 @@ const docTemplate = `{
                                     "type": "object",
                                     "properties": {
                                         "data": {
-                                            "$ref": "#/definitions/model.GroupPublicMCPReusingParam"
+                                            "$ref": "#/definitions/model.PublicMCPReusingParam"
                                         }
                                     }
                                 }
@@ -3528,7 +3812,7 @@ const docTemplate = `{
                         "in": "body",
                         "required": true,
                         "schema": {
-                            "$ref": "#/definitions/model.GroupPublicMCPReusingParam"
+                            "$ref": "#/definitions/model.PublicMCPReusingParam"
                         }
                     }
                 ],
@@ -6003,18 +6287,30 @@ const docTemplate = `{
                 }
             }
         },
-        "/mcp/message": {
+        "/mcp/group/message": {
             "post": {
                 "summary": "MCP SSE Proxy",
                 "responses": {}
             }
         },
-        "/mcp/public/{id}/sse": {
+        "/mcp/group/{id}/sse": {
             "get": {
+                "summary": "Group MCP SSE Server",
+                "responses": {}
+            }
+        },
+        "/mcp/public/message": {
+            "post": {
                 "summary": "MCP SSE Proxy",
                 "responses": {}
             }
         },
+        "/mcp/public/{id}/sse": {
+            "get": {
+                "summary": "Public MCP SSE Server",
+                "responses": {}
+            }
+        },
         "/v1/audio/speech": {
             "post": {
                 "security": [
@@ -8013,6 +8309,66 @@ const docTemplate = `{
                 }
             }
         },
+        "model.GroupMCP": {
+            "type": "object",
+            "properties": {
+                "created_at": {
+                    "type": "string"
+                },
+                "group_id": {
+                    "type": "string"
+                },
+                "id": {
+                    "type": "string"
+                },
+                "name": {
+                    "type": "string"
+                },
+                "openapi_config": {
+                    "$ref": "#/definitions/model.MCPOpenAPIConfig"
+                },
+                "proxy_sse_config": {
+                    "$ref": "#/definitions/model.GroupMCPProxySSEConfig"
+                },
+                "type": {
+                    "$ref": "#/definitions/model.GroupMCPType"
+                },
+                "update_at": {
+                    "type": "string"
+                }
+            }
+        },
+        "model.GroupMCPProxySSEConfig": {
+            "type": "object",
+            "properties": {
+                "headers": {
+                    "type": "object",
+                    "additionalProperties": {
+                        "type": "string"
+                    }
+                },
+                "querys": {
+                    "type": "object",
+                    "additionalProperties": {
+                        "type": "string"
+                    }
+                },
+                "url": {
+                    "type": "string"
+                }
+            }
+        },
+        "model.GroupMCPType": {
+            "type": "string",
+            "enum": [
+                "mcp_proxy_sse",
+                "mcp_openapi"
+            ],
+            "x-enum-varnames": [
+                "GroupMCPTypeProxySSE",
+                "GroupMCPTypeOpenAPI"
+            ]
+        },
         "model.GroupModelConfig": {
             "type": "object",
             "properties": {
@@ -8051,23 +8407,6 @@ const docTemplate = `{
                 }
             }
         },
-        "model.GroupPublicMCPReusingParam": {
-            "type": "object",
-            "properties": {
-                "group_id": {
-                    "type": "string"
-                },
-                "mcp_id": {
-                    "type": "string"
-                },
-                "reusing_params": {
-                    "type": "object",
-                    "additionalProperties": {
-                        "type": "string"
-                    }
-                }
-            }
-        },
         "model.ImageData": {
             "type": "object",
             "properties": {
@@ -8229,9 +8568,6 @@ const docTemplate = `{
                 "openapi_spec": {
                     "type": "string"
                 },
-                "price": {
-                    "$ref": "#/definitions/model.MCPPrice"
-                },
                 "server": {
                     "type": "string"
                 },
@@ -8243,10 +8579,10 @@ const docTemplate = `{
         "model.MCPPrice": {
             "type": "object",
             "properties": {
-                "defaultToolsCallPrice": {
+                "default_tools_call_price": {
                     "type": "number"
                 },
-                "toolsCallPrices": {
+                "tools_call_prices": {
                     "type": "object",
                     "additionalProperties": {
                         "type": "number"
@@ -8532,6 +8868,9 @@ const docTemplate = `{
                 "openapi_config": {
                     "$ref": "#/definitions/model.MCPOpenAPIConfig"
                 },
+                "price": {
+                    "$ref": "#/definitions/model.MCPPrice"
+                },
                 "proxy_sse_config": {
                     "$ref": "#/definitions/model.PublicMCPProxySSEConfig"
                 },
@@ -8567,9 +8906,6 @@ const docTemplate = `{
                         "type": "string"
                     }
                 },
-                "price": {
-                    "$ref": "#/definitions/model.MCPPrice"
-                },
                 "querys": {
                     "type": "object",
                     "additionalProperties": {
@@ -8587,6 +8923,29 @@ const docTemplate = `{
                 }
             }
         },
+        "model.PublicMCPReusingParam": {
+            "type": "object",
+            "properties": {
+                "created_at": {
+                    "type": "string"
+                },
+                "group_id": {
+                    "type": "string"
+                },
+                "mcp_id": {
+                    "type": "string"
+                },
+                "reusing_params": {
+                    "type": "object",
+                    "additionalProperties": {
+                        "type": "string"
+                    }
+                },
+                "update_at": {
+                    "type": "string"
+                }
+            }
+        },
         "model.PublicMCPType": {
             "type": "string",
             "enum": [

+ 388 - 29
core/docs/swagger.json

@@ -3204,6 +3204,290 @@
                 }
             }
         },
+        "/api/mcp/group/{group}": {
+            "get": {
+                "security": [
+                    {
+                        "ApiKeyAuth": []
+                    }
+                ],
+                "description": "Get a list of Group MCPs with pagination and filtering",
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "mcp"
+                ],
+                "summary": "Get Group MCPs",
+                "parameters": [
+                    {
+                        "type": "string",
+                        "description": "Group ID",
+                        "name": "group",
+                        "in": "path",
+                        "required": true
+                    },
+                    {
+                        "type": "integer",
+                        "description": "Page number",
+                        "name": "page",
+                        "in": "query"
+                    },
+                    {
+                        "type": "integer",
+                        "description": "Items per page",
+                        "name": "per_page",
+                        "in": "query"
+                    },
+                    {
+                        "type": "string",
+                        "description": "MCP type",
+                        "name": "type",
+                        "in": "query"
+                    },
+                    {
+                        "type": "string",
+                        "description": "Search keyword",
+                        "name": "keyword",
+                        "in": "query"
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "allOf": [
+                                {
+                                    "$ref": "#/definitions/middleware.APIResponse"
+                                },
+                                {
+                                    "type": "object",
+                                    "properties": {
+                                        "data": {
+                                            "type": "array",
+                                            "items": {
+                                                "$ref": "#/definitions/model.GroupMCP"
+                                            }
+                                        }
+                                    }
+                                }
+                            ]
+                        }
+                    }
+                }
+            },
+            "post": {
+                "security": [
+                    {
+                        "ApiKeyAuth": []
+                    }
+                ],
+                "description": "Create a new Group MCP",
+                "consumes": [
+                    "application/json"
+                ],
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "mcp"
+                ],
+                "summary": "Create Group MCP",
+                "parameters": [
+                    {
+                        "type": "string",
+                        "description": "Group ID",
+                        "name": "group",
+                        "in": "path",
+                        "required": true
+                    },
+                    {
+                        "description": "Group MCP object",
+                        "name": "mcp",
+                        "in": "body",
+                        "required": true,
+                        "schema": {
+                            "$ref": "#/definitions/model.GroupMCP"
+                        }
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "allOf": [
+                                {
+                                    "$ref": "#/definitions/middleware.APIResponse"
+                                },
+                                {
+                                    "type": "object",
+                                    "properties": {
+                                        "data": {
+                                            "$ref": "#/definitions/model.GroupMCP"
+                                        }
+                                    }
+                                }
+                            ]
+                        }
+                    }
+                }
+            }
+        },
+        "/api/mcp/group/{group}/{id}": {
+            "get": {
+                "security": [
+                    {
+                        "ApiKeyAuth": []
+                    }
+                ],
+                "description": "Get a specific Group MCP by its ID and Group ID",
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "mcp"
+                ],
+                "summary": "Get Group MCP by ID",
+                "parameters": [
+                    {
+                        "type": "string",
+                        "description": "MCP ID",
+                        "name": "id",
+                        "in": "path",
+                        "required": true
+                    },
+                    {
+                        "type": "string",
+                        "description": "Group ID",
+                        "name": "group",
+                        "in": "path",
+                        "required": true
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "allOf": [
+                                {
+                                    "$ref": "#/definitions/middleware.APIResponse"
+                                },
+                                {
+                                    "type": "object",
+                                    "properties": {
+                                        "data": {
+                                            "$ref": "#/definitions/model.GroupMCP"
+                                        }
+                                    }
+                                }
+                            ]
+                        }
+                    }
+                }
+            },
+            "put": {
+                "security": [
+                    {
+                        "ApiKeyAuth": []
+                    }
+                ],
+                "description": "Update an existing Group MCP",
+                "consumes": [
+                    "application/json"
+                ],
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "mcp"
+                ],
+                "summary": "Update Group MCP",
+                "parameters": [
+                    {
+                        "type": "string",
+                        "description": "MCP ID",
+                        "name": "id",
+                        "in": "path",
+                        "required": true
+                    },
+                    {
+                        "type": "string",
+                        "description": "Group ID",
+                        "name": "group",
+                        "in": "path",
+                        "required": true
+                    },
+                    {
+                        "description": "Group MCP object",
+                        "name": "mcp",
+                        "in": "body",
+                        "required": true,
+                        "schema": {
+                            "$ref": "#/definitions/model.GroupMCP"
+                        }
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "allOf": [
+                                {
+                                    "$ref": "#/definitions/middleware.APIResponse"
+                                },
+                                {
+                                    "type": "object",
+                                    "properties": {
+                                        "data": {
+                                            "$ref": "#/definitions/model.GroupMCP"
+                                        }
+                                    }
+                                }
+                            ]
+                        }
+                    }
+                }
+            },
+            "delete": {
+                "security": [
+                    {
+                        "ApiKeyAuth": []
+                    }
+                ],
+                "description": "Delete a Group MCP by ID and Group ID",
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "mcp"
+                ],
+                "summary": "Delete Group MCP",
+                "parameters": [
+                    {
+                        "type": "string",
+                        "description": "MCP ID",
+                        "name": "id",
+                        "in": "path",
+                        "required": true
+                    },
+                    {
+                        "type": "string",
+                        "description": "Group ID",
+                        "name": "group",
+                        "in": "path",
+                        "required": true
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "$ref": "#/definitions/middleware.APIResponse"
+                        }
+                    }
+                }
+            }
+        },
         "/api/mcp/public/": {
             "get": {
                 "security": [
@@ -3472,7 +3756,7 @@
                                     "type": "object",
                                     "properties": {
                                         "data": {
-                                            "$ref": "#/definitions/model.GroupPublicMCPReusingParam"
+                                            "$ref": "#/definitions/model.PublicMCPReusingParam"
                                         }
                                     }
                                 }
@@ -3519,7 +3803,7 @@
                         "in": "body",
                         "required": true,
                         "schema": {
-                            "$ref": "#/definitions/model.GroupPublicMCPReusingParam"
+                            "$ref": "#/definitions/model.PublicMCPReusingParam"
                         }
                     }
                 ],
@@ -5994,18 +6278,30 @@
                 }
             }
         },
-        "/mcp/message": {
+        "/mcp/group/message": {
             "post": {
                 "summary": "MCP SSE Proxy",
                 "responses": {}
             }
         },
-        "/mcp/public/{id}/sse": {
+        "/mcp/group/{id}/sse": {
             "get": {
+                "summary": "Group MCP SSE Server",
+                "responses": {}
+            }
+        },
+        "/mcp/public/message": {
+            "post": {
                 "summary": "MCP SSE Proxy",
                 "responses": {}
             }
         },
+        "/mcp/public/{id}/sse": {
+            "get": {
+                "summary": "Public MCP SSE Server",
+                "responses": {}
+            }
+        },
         "/v1/audio/speech": {
             "post": {
                 "security": [
@@ -8004,6 +8300,66 @@
                 }
             }
         },
+        "model.GroupMCP": {
+            "type": "object",
+            "properties": {
+                "created_at": {
+                    "type": "string"
+                },
+                "group_id": {
+                    "type": "string"
+                },
+                "id": {
+                    "type": "string"
+                },
+                "name": {
+                    "type": "string"
+                },
+                "openapi_config": {
+                    "$ref": "#/definitions/model.MCPOpenAPIConfig"
+                },
+                "proxy_sse_config": {
+                    "$ref": "#/definitions/model.GroupMCPProxySSEConfig"
+                },
+                "type": {
+                    "$ref": "#/definitions/model.GroupMCPType"
+                },
+                "update_at": {
+                    "type": "string"
+                }
+            }
+        },
+        "model.GroupMCPProxySSEConfig": {
+            "type": "object",
+            "properties": {
+                "headers": {
+                    "type": "object",
+                    "additionalProperties": {
+                        "type": "string"
+                    }
+                },
+                "querys": {
+                    "type": "object",
+                    "additionalProperties": {
+                        "type": "string"
+                    }
+                },
+                "url": {
+                    "type": "string"
+                }
+            }
+        },
+        "model.GroupMCPType": {
+            "type": "string",
+            "enum": [
+                "mcp_proxy_sse",
+                "mcp_openapi"
+            ],
+            "x-enum-varnames": [
+                "GroupMCPTypeProxySSE",
+                "GroupMCPTypeOpenAPI"
+            ]
+        },
         "model.GroupModelConfig": {
             "type": "object",
             "properties": {
@@ -8042,23 +8398,6 @@
                 }
             }
         },
-        "model.GroupPublicMCPReusingParam": {
-            "type": "object",
-            "properties": {
-                "group_id": {
-                    "type": "string"
-                },
-                "mcp_id": {
-                    "type": "string"
-                },
-                "reusing_params": {
-                    "type": "object",
-                    "additionalProperties": {
-                        "type": "string"
-                    }
-                }
-            }
-        },
         "model.ImageData": {
             "type": "object",
             "properties": {
@@ -8220,9 +8559,6 @@
                 "openapi_spec": {
                     "type": "string"
                 },
-                "price": {
-                    "$ref": "#/definitions/model.MCPPrice"
-                },
                 "server": {
                     "type": "string"
                 },
@@ -8234,10 +8570,10 @@
         "model.MCPPrice": {
             "type": "object",
             "properties": {
-                "defaultToolsCallPrice": {
+                "default_tools_call_price": {
                     "type": "number"
                 },
-                "toolsCallPrices": {
+                "tools_call_prices": {
                     "type": "object",
                     "additionalProperties": {
                         "type": "number"
@@ -8523,6 +8859,9 @@
                 "openapi_config": {
                     "$ref": "#/definitions/model.MCPOpenAPIConfig"
                 },
+                "price": {
+                    "$ref": "#/definitions/model.MCPPrice"
+                },
                 "proxy_sse_config": {
                     "$ref": "#/definitions/model.PublicMCPProxySSEConfig"
                 },
@@ -8558,9 +8897,6 @@
                         "type": "string"
                     }
                 },
-                "price": {
-                    "$ref": "#/definitions/model.MCPPrice"
-                },
                 "querys": {
                     "type": "object",
                     "additionalProperties": {
@@ -8578,6 +8914,29 @@
                 }
             }
         },
+        "model.PublicMCPReusingParam": {
+            "type": "object",
+            "properties": {
+                "created_at": {
+                    "type": "string"
+                },
+                "group_id": {
+                    "type": "string"
+                },
+                "mcp_id": {
+                    "type": "string"
+                },
+                "reusing_params": {
+                    "type": "object",
+                    "additionalProperties": {
+                        "type": "string"
+                    }
+                },
+                "update_at": {
+                    "type": "string"
+                }
+            }
+        },
         "model.PublicMCPType": {
             "type": "string",
             "enum": [

+ 241 - 20
core/docs/swagger.yaml

@@ -793,6 +793,46 @@ definitions:
       web_search_count:
         type: integer
     type: object
+  model.GroupMCP:
+    properties:
+      created_at:
+        type: string
+      group_id:
+        type: string
+      id:
+        type: string
+      name:
+        type: string
+      openapi_config:
+        $ref: '#/definitions/model.MCPOpenAPIConfig'
+      proxy_sse_config:
+        $ref: '#/definitions/model.GroupMCPProxySSEConfig'
+      type:
+        $ref: '#/definitions/model.GroupMCPType'
+      update_at:
+        type: string
+    type: object
+  model.GroupMCPProxySSEConfig:
+    properties:
+      headers:
+        additionalProperties:
+          type: string
+        type: object
+      querys:
+        additionalProperties:
+          type: string
+        type: object
+      url:
+        type: string
+    type: object
+  model.GroupMCPType:
+    enum:
+    - mcp_proxy_sse
+    - mcp_openapi
+    type: string
+    x-enum-varnames:
+    - GroupMCPTypeProxySSE
+    - GroupMCPTypeOpenAPI
   model.GroupModelConfig:
     properties:
       group_id:
@@ -818,17 +858,6 @@ definitions:
       tpm:
         type: integer
     type: object
-  model.GroupPublicMCPReusingParam:
-    properties:
-      group_id:
-        type: string
-      mcp_id:
-        type: string
-      reusing_params:
-        additionalProperties:
-          type: string
-        type: object
-    type: object
   model.ImageData:
     properties:
       b64_json:
@@ -935,8 +964,6 @@ definitions:
         type: string
       openapi_spec:
         type: string
-      price:
-        $ref: '#/definitions/model.MCPPrice'
       server:
         type: string
       v2:
@@ -944,9 +971,9 @@ definitions:
     type: object
   model.MCPPrice:
     properties:
-      defaultToolsCallPrice:
+      default_tools_call_price:
         type: number
-      toolsCallPrices:
+      tools_call_prices:
         additionalProperties:
           type: number
         type: object
@@ -1156,6 +1183,8 @@ definitions:
         type: string
       openapi_config:
         $ref: '#/definitions/model.MCPOpenAPIConfig'
+      price:
+        $ref: '#/definitions/model.MCPPrice'
       proxy_sse_config:
         $ref: '#/definitions/model.PublicMCPProxySSEConfig'
       readme:
@@ -1179,8 +1208,6 @@ definitions:
         additionalProperties:
           type: string
         type: object
-      price:
-        $ref: '#/definitions/model.MCPPrice'
       querys:
         additionalProperties:
           type: string
@@ -1192,6 +1219,21 @@ definitions:
       url:
         type: string
     type: object
+  model.PublicMCPReusingParam:
+    properties:
+      created_at:
+        type: string
+      group_id:
+        type: string
+      mcp_id:
+        type: string
+      reusing_params:
+        additionalProperties:
+          type: string
+        type: object
+      update_at:
+        type: string
+    type: object
   model.PublicMCPType:
     enum:
     - mcp_proxy_sse
@@ -3317,6 +3359,177 @@ paths:
       summary: Get used token names
       tags:
       - logs
+  /api/mcp/group/{group}:
+    get:
+      description: Get a list of Group MCPs with pagination and filtering
+      parameters:
+      - description: Group ID
+        in: path
+        name: group
+        required: true
+        type: string
+      - description: Page number
+        in: query
+        name: page
+        type: integer
+      - description: Items per page
+        in: query
+        name: per_page
+        type: integer
+      - description: MCP type
+        in: query
+        name: type
+        type: string
+      - description: Search keyword
+        in: query
+        name: keyword
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            allOf:
+            - $ref: '#/definitions/middleware.APIResponse'
+            - properties:
+                data:
+                  items:
+                    $ref: '#/definitions/model.GroupMCP'
+                  type: array
+              type: object
+      security:
+      - ApiKeyAuth: []
+      summary: Get Group MCPs
+      tags:
+      - mcp
+    post:
+      consumes:
+      - application/json
+      description: Create a new Group MCP
+      parameters:
+      - description: Group ID
+        in: path
+        name: group
+        required: true
+        type: string
+      - description: Group MCP object
+        in: body
+        name: mcp
+        required: true
+        schema:
+          $ref: '#/definitions/model.GroupMCP'
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            allOf:
+            - $ref: '#/definitions/middleware.APIResponse'
+            - properties:
+                data:
+                  $ref: '#/definitions/model.GroupMCP'
+              type: object
+      security:
+      - ApiKeyAuth: []
+      summary: Create Group MCP
+      tags:
+      - mcp
+  /api/mcp/group/{group}/{id}:
+    delete:
+      description: Delete a Group MCP by ID and Group ID
+      parameters:
+      - description: MCP ID
+        in: path
+        name: id
+        required: true
+        type: string
+      - description: Group ID
+        in: path
+        name: group
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/middleware.APIResponse'
+      security:
+      - ApiKeyAuth: []
+      summary: Delete Group MCP
+      tags:
+      - mcp
+    get:
+      description: Get a specific Group MCP by its ID and Group ID
+      parameters:
+      - description: MCP ID
+        in: path
+        name: id
+        required: true
+        type: string
+      - description: Group ID
+        in: path
+        name: group
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            allOf:
+            - $ref: '#/definitions/middleware.APIResponse'
+            - properties:
+                data:
+                  $ref: '#/definitions/model.GroupMCP'
+              type: object
+      security:
+      - ApiKeyAuth: []
+      summary: Get Group MCP by ID
+      tags:
+      - mcp
+    put:
+      consumes:
+      - application/json
+      description: Update an existing Group MCP
+      parameters:
+      - description: MCP ID
+        in: path
+        name: id
+        required: true
+        type: string
+      - description: Group ID
+        in: path
+        name: group
+        required: true
+        type: string
+      - description: Group MCP object
+        in: body
+        name: mcp
+        required: true
+        schema:
+          $ref: '#/definitions/model.GroupMCP'
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            allOf:
+            - $ref: '#/definitions/middleware.APIResponse'
+            - properties:
+                data:
+                  $ref: '#/definitions/model.GroupMCP'
+              type: object
+      security:
+      - ApiKeyAuth: []
+      summary: Update Group MCP
+      tags:
+      - mcp
   /api/mcp/public/:
     get:
       description: Get a list of MCPs with pagination and filtering
@@ -3477,7 +3690,7 @@ paths:
             - $ref: '#/definitions/middleware.APIResponse'
             - properties:
                 data:
-                  $ref: '#/definitions/model.GroupPublicMCPReusingParam'
+                  $ref: '#/definitions/model.PublicMCPReusingParam'
               type: object
       security:
       - ApiKeyAuth: []
@@ -3504,7 +3717,7 @@ paths:
         name: params
         required: true
         schema:
-          $ref: '#/definitions/model.GroupPublicMCPReusingParam'
+          $ref: '#/definitions/model.PublicMCPReusingParam'
       produces:
       - application/json
       responses:
@@ -4982,12 +5195,20 @@ paths:
       summary: Search tokens
       tags:
       - tokens
-  /mcp/message:
+  /mcp/group/{id}/sse:
+    get:
+      responses: {}
+      summary: Group MCP SSE Server
+  /mcp/group/message:
     post:
       responses: {}
       summary: MCP SSE Proxy
   /mcp/public/{id}/sse:
     get:
+      responses: {}
+      summary: Public MCP SSE Server
+  /mcp/public/message:
+    post:
       responses: {}
       summary: MCP SSE Proxy
   /v1/audio/speech:

+ 4 - 3
core/go.mod

@@ -18,8 +18,8 @@ require (
 	github.com/jinzhu/copier v0.4.0
 	github.com/joho/godotenv v1.5.1
 	github.com/json-iterator/go v1.1.12
-	github.com/labring/aiproxy/openapi-mcp v0.0.0-00010101000000-000000000000
-	github.com/mark3labs/mcp-go v0.21.1
+	github.com/labring/aiproxy/openapi-mcp v0.0.0-20250418172834-d947728e0e51
+	github.com/mark3labs/mcp-go v0.22.0
 	github.com/maruel/natural v1.1.1
 	github.com/mattn/go-isatty v0.0.20
 	github.com/patrickmn/go-cache v2.1.0+incompatible
@@ -102,6 +102,7 @@ require (
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
 	github.com/smarty/assertions v1.15.0 // indirect
+	github.com/spf13/cast v1.7.1 // indirect
 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
 	github.com/ugorji/go/codec v1.2.12 // indirect
 	github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
@@ -122,7 +123,7 @@ require (
 	golang.org/x/tools v0.32.0 // indirect
 	google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e // indirect
-	google.golang.org/grpc v1.71.1 // indirect
+	google.golang.org/grpc v1.72.0 // indirect
 	google.golang.org/protobuf v1.36.6 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 	modernc.org/libc v1.63.0 // indirect

+ 8 - 4
core/go.sum

@@ -49,6 +49,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
+github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
 github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
 github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
 github.com/getkin/kin-openapi v0.131.0 h1:NO2UeHnFKRYhZ8wg6Nyh5Cq7dHk4suQQr72a4pMrDxE=
@@ -148,8 +150,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
 github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
 github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
 github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
-github.com/mark3labs/mcp-go v0.21.1 h1:7Ek6KPIIbMhEYHRiRIg6K6UAgNZCJaHKQp926MNr6V0=
-github.com/mark3labs/mcp-go v0.21.1/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE=
+github.com/mark3labs/mcp-go v0.22.0 h1:cCEBWi4Yy9Kio+OW1hWIyi4WLsSr+RBBK6FI5tj+b7I=
+github.com/mark3labs/mcp-go v0.22.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
 github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo=
 github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
@@ -193,6 +195,8 @@ github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGB
 github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec=
 github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY=
 github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60=
+github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
+github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
 github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
 github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
 github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
@@ -294,8 +298,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e h1:
 google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e/go.mod h1:085qFyf2+XaZlRdCgKNCIZ3afY2p4HHZdoIRpId8F4A=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e h1:ztQaXfzEXTmCBvbtWYRhJxW+0iJcz2qXfd38/e9l7bA=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
-google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI=
-google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
+google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
+google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
 google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
 google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

+ 17 - 12
core/model/group.go

@@ -22,17 +22,18 @@ const (
 )
 
 type Group struct {
-	CreatedAt             time.Time                    `json:"created_at"`
-	ID                    string                       `gorm:"primaryKey"                    json:"id"`
-	Tokens                []Token                      `gorm:"foreignKey:GroupID"            json:"-"`
-	GroupModelConfigs     []GroupModelConfig           `gorm:"foreignKey:GroupID"            json:"-"`
-	GroupMCPReusingParams []GroupPublicMCPReusingParam `gorm:"foreignKey:GroupID"            json:"-"`
-	Status                int                          `gorm:"default:1;index"               json:"status"`
-	RPMRatio              float64                      `gorm:"index"                         json:"rpm_ratio,omitempty"`
-	TPMRatio              float64                      `gorm:"index"                         json:"tpm_ratio,omitempty"`
-	UsedAmount            float64                      `gorm:"index"                         json:"used_amount"`
-	RequestCount          int                          `gorm:"index"                         json:"request_count"`
-	AvailableSets         []string                     `gorm:"serializer:fastjson;type:text" json:"available_sets,omitempty"`
+	CreatedAt              time.Time               `json:"created_at"`
+	ID                     string                  `gorm:"primaryKey"                    json:"id"`
+	Tokens                 []Token                 `gorm:"foreignKey:GroupID"            json:"-"`
+	GroupModelConfigs      []GroupModelConfig      `gorm:"foreignKey:GroupID"            json:"-"`
+	PublicMCPReusingParams []PublicMCPReusingParam `gorm:"foreignKey:GroupID"            json:"-"`
+	GroupMCPs              []GroupMCP              `gorm:"foreignKey:GroupID"            json:"-"`
+	Status                 int                     `gorm:"default:1;index"               json:"status"`
+	RPMRatio               float64                 `gorm:"index"                         json:"rpm_ratio,omitempty"`
+	TPMRatio               float64                 `gorm:"index"                         json:"tpm_ratio,omitempty"`
+	UsedAmount             float64                 `gorm:"index"                         json:"used_amount"`
+	RequestCount           int                     `gorm:"index"                         json:"request_count"`
+	AvailableSets          []string                `gorm:"serializer:fastjson;type:text" json:"available_sets,omitempty"`
 
 	BalanceAlertEnabled   bool    `gorm:"default:false" json:"balance_alert_enabled"`
 	BalanceAlertThreshold float64 `gorm:"default:0"     json:"balance_alert_threshold"`
@@ -43,7 +44,11 @@ func (g *Group) BeforeDelete(tx *gorm.DB) (err error) {
 	if err != nil {
 		return err
 	}
-	err = tx.Model(&GroupPublicMCPReusingParam{}).Where("group_id = ?", g.ID).Delete(&GroupPublicMCPReusingParam{}).Error
+	err = tx.Model(&PublicMCPReusingParam{}).Where("group_id = ?", g.ID).Delete(&PublicMCPReusingParam{}).Error
+	if err != nil {
+		return err
+	}
+	err = tx.Model(&GroupMCP{}).Where("group_id = ?", g.ID).Delete(&GroupMCP{}).Error
 	if err != nil {
 		return err
 	}

+ 165 - 0
core/model/groupmcp.go

@@ -0,0 +1,165 @@
+package model
+
+import (
+	"errors"
+	"time"
+
+	"github.com/bytedance/sonic"
+	"github.com/labring/aiproxy/core/common"
+	"gorm.io/gorm"
+)
+
+const (
+	ErrGroupMCPNotFound = "group mcp"
+)
+
+type GroupMCPType string
+
+const (
+	GroupMCPTypeProxySSE GroupMCPType = "mcp_proxy_sse"
+	GroupMCPTypeOpenAPI  GroupMCPType = "mcp_openapi"
+)
+
+type GroupMCPProxySSEConfig struct {
+	URL     string            `json:"url"`
+	Querys  map[string]string `json:"querys"`
+	Headers map[string]string `json:"headers"`
+}
+
+type GroupMCP struct {
+	ID             string                  `gorm:"primaryKey"                    json:"id"`
+	GroupID        string                  `gorm:"primaryKey"                    json:"group_id"`
+	Group          *Group                  `gorm:"foreignKey:GroupID"            json:"-"`
+	CreatedAt      time.Time               `gorm:"index"                         json:"created_at"`
+	UpdateAt       time.Time               `gorm:"index"                         json:"update_at"`
+	Name           string                  `json:"name"`
+	Type           GroupMCPType            `gorm:"index"                         json:"type"`
+	ProxySSEConfig *GroupMCPProxySSEConfig `gorm:"serializer:fastjson;type:text" json:"proxy_sse_config,omitempty"`
+	OpenAPIConfig  *MCPOpenAPIConfig       `gorm:"serializer:fastjson;type:text" json:"openapi_config,omitempty"`
+}
+
+func (g *GroupMCP) BeforeCreate(_ *gorm.DB) (err error) {
+	if g.GroupID == "" {
+		return errors.New("group id is empty")
+	}
+	if g.ID == "" {
+		g.ID = common.ShortUUID()
+	}
+
+	if g.OpenAPIConfig != nil {
+		config := g.OpenAPIConfig
+		if config.OpenAPISpec != "" {
+			return validateHTTPURL(config.OpenAPISpec)
+		}
+		if config.OpenAPIContent != "" {
+			return nil
+		}
+		return errors.New("openapi spec and content is empty")
+	}
+
+	if g.ProxySSEConfig != nil {
+		config := g.ProxySSEConfig
+		return validateHTTPURL(config.URL)
+	}
+
+	return
+}
+
+func (g *GroupMCP) MarshalJSON() ([]byte, error) {
+	type Alias GroupMCP
+	a := &struct {
+		*Alias
+		CreatedAt int64 `json:"created_at"`
+		UpdateAt  int64 `json:"update_at"`
+	}{
+		Alias:     (*Alias)(g),
+		CreatedAt: g.CreatedAt.UnixMilli(),
+		UpdateAt:  g.UpdateAt.UnixMilli(),
+	}
+	return sonic.Marshal(a)
+}
+
+// CreateGroupMCP creates a new GroupMCP
+func CreateGroupMCP(mcp *GroupMCP) error {
+	err := DB.Create(mcp).Error
+	if err != nil && errors.Is(err, gorm.ErrDuplicatedKey) {
+		return errors.New("group mcp already exists")
+	}
+	return err
+}
+
+// UpdateGroupMCP updates an existing GroupMCP
+func UpdateGroupMCP(mcp *GroupMCP) error {
+	selects := []string{
+		"name",
+		"proxy_sse_config",
+		"openapi_config",
+	}
+	if mcp.Type != "" {
+		selects = append(selects, "type")
+	}
+	result := DB.
+		Select(selects).
+		Where("id = ? AND group_id = ?", mcp.ID, mcp.GroupID).
+		Updates(mcp)
+	return HandleUpdateResult(result, ErrGroupMCPNotFound)
+}
+
+// DeleteGroupMCP deletes a GroupMCP by ID and GroupID
+func DeleteGroupMCP(id string, groupID string) error {
+	if id == "" || groupID == "" {
+		return errors.New("group mcp id or group id is empty")
+	}
+	result := DB.Where("id = ? AND group_id = ?", id, groupID).Delete(&GroupMCP{})
+	return HandleUpdateResult(result, ErrGroupMCPNotFound)
+}
+
+// GetGroupMCPByID retrieves a GroupMCP by ID and GroupID
+func GetGroupMCPByID(id string, groupID string) (*GroupMCP, error) {
+	if id == "" || groupID == "" {
+		return nil, errors.New("group mcp id or group id is empty")
+	}
+	var mcp GroupMCP
+	err := DB.Where("id = ? AND group_id = ?", id, groupID).First(&mcp).Error
+	return &mcp, HandleNotFound(err, ErrGroupMCPNotFound)
+}
+
+// GetGroupMCPs retrieves GroupMCPs with pagination and filtering
+func GetGroupMCPs(groupID string, page int, perPage int, mcpType PublicMCPType, keyword string) (mcps []*GroupMCP, total int64, err error) {
+	if groupID == "" {
+		return nil, 0, errors.New("group id is empty")
+	}
+
+	tx := DB.Model(&GroupMCP{}).Where("group_id = ?", groupID)
+
+	if mcpType != "" {
+		tx = tx.Where("type = ?", mcpType)
+	}
+
+	if keyword != "" {
+		keyword = "%" + keyword + "%"
+		if common.UsingPostgreSQL {
+			tx = tx.Where("name ILIKE ? OR id ILIKE ?", keyword, keyword)
+		} else {
+			tx = tx.Where("name LIKE ? OR id LIKE ?", keyword, keyword)
+		}
+	}
+
+	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
+}

+ 2 - 1
core/model/main.go

@@ -140,7 +140,8 @@ func migrateDB() error {
 		&Token{},
 		&PublicMCP{},
 		&GroupModelConfig{},
-		&GroupPublicMCPReusingParam{},
+		&PublicMCPReusingParam{},
+		&GroupMCP{},
 		&Group{},
 		&Option{},
 		&ModelConfig{},

+ 111 - 41
core/model/publicmcp.go

@@ -2,14 +2,17 @@ package model
 
 import (
 	"errors"
+	"net/url"
 	"time"
 
+	"github.com/bytedance/sonic"
+	"github.com/labring/aiproxy/core/common"
 	"gorm.io/gorm"
 )
 
 const (
-	ErrPublicMCPNotFound            = "public mcp"
-	ErrGroupMCPReusingParamNotFound = "group mcp reusing param"
+	ErrPublicMCPNotFound       = "public mcp"
+	ErrMCPReusingParamNotFound = "mcp reusing param"
 )
 
 type PublicMCPType string
@@ -35,8 +38,8 @@ type ReusingParam struct {
 }
 
 type MCPPrice struct {
-	DefaultToolsCallPrice float64
-	ToolsCallPrices       map[string]float64
+	DefaultToolsCallPrice float64            `json:"default_tools_call_price"`
+	ToolsCallPrices       map[string]float64 `gorm:"serializer:fastjson;type:text" json:"tools_call_prices"`
 }
 
 type PublicMCPProxySSEConfig struct {
@@ -44,17 +47,18 @@ type PublicMCPProxySSEConfig struct {
 	Querys        map[string]string       `json:"querys"`
 	Headers       map[string]string       `json:"headers"`
 	ReusingParams map[string]ReusingParam `json:"reusing_params"`
-	Price         MCPPrice                `json:"price"`
 }
 
-type GroupPublicMCPReusingParam struct {
+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 (l *GroupPublicMCPReusingParam) BeforeCreate(_ *gorm.DB) (err error) {
+func (l *PublicMCPReusingParam) BeforeCreate(_ *gorm.DB) (err error) {
 	if l.MCPID == "" {
 		return errors.New("mcp id is empty")
 	}
@@ -64,41 +68,99 @@ func (l *GroupPublicMCPReusingParam) BeforeCreate(_ *gorm.DB) (err error) {
 	return
 }
 
+func (l *PublicMCPReusingParam) MarshalJSON() ([]byte, error) {
+	type Alias PublicMCPReusingParam
+	a := &struct {
+		*Alias
+		CreatedAt int64 `json:"created_at"`
+		UpdateAt  int64 `json:"update_at"`
+	}{
+		Alias:     (*Alias)(l),
+		CreatedAt: l.CreatedAt.UnixMilli(),
+		UpdateAt:  l.UpdateAt.UnixMilli(),
+	}
+	return sonic.Marshal(a)
+}
+
 type MCPOpenAPIConfig struct {
-	OpenAPISpec    string   `json:"openapi_spec"`
-	OpenAPIContent string   `json:"openapi_content,omitempty"`
-	V2             bool     `json:"v2"`
-	Server         string   `json:"server,omitempty"`
-	Authorization  string   `json:"authorization,omitempty"`
-	Price          MCPPrice `json:"price"`
+	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 PublicMCP struct {
-	ID                          string                       `gorm:"primaryKey"                    json:"id"`
-	CreatedAt                   time.Time                    `gorm:"index"                         json:"created_at"`
-	UpdateAt                    time.Time                    `gorm:"index"                         json:"update_at"`
-	GroupPublicMCPReusingParams []GroupPublicMCPReusingParam `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"`
-	Author                      string                       `json:"author"`
-	LogoURL                     string                       `json:"logo_url"`
-	ProxySSEConfig              *PublicMCPProxySSEConfig     `gorm:"serializer:fastjson;type:text" json:"proxy_sse_config,omitempty"`
-	OpenAPIConfig               *MCPOpenAPIConfig            `gorm:"serializer:fastjson;type:text" json:"openapi_config,omitempty"`
-}
-
-func (p *PublicMCP) BeforeCreate(_ *gorm.DB) (err error) {
+	ID                     string                   `gorm:"primaryKey"                    json:"id"`
+	CreatedAt              time.Time                `gorm:"index"                         json:"created_at"`
+	UpdateAt               time.Time                `gorm:"index"                         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"`
+	Author                 string                   `json:"author"`
+	LogoURL                string                   `json:"logo_url"`
+	Price                  MCPPrice                 `gorm:"embedded"                      json:"price"`
+	ProxySSEConfig         *PublicMCPProxySSEConfig `gorm:"serializer:fastjson;type:text" json:"proxy_sse_config,omitempty"`
+	OpenAPIConfig          *MCPOpenAPIConfig        `gorm:"serializer:fastjson;type:text" json:"openapi_config,omitempty"`
+}
+
+func (p *PublicMCP) BeforeCreate(_ *gorm.DB) error {
 	if p.ID == "" {
 		return errors.New("mcp id is empty")
 	}
-	return
+
+	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.ProxySSEConfig != nil {
+		config := p.ProxySSEConfig
+		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(&GroupPublicMCPReusingParam{}).Where("mcp_id = ?", p.ID).Delete(&GroupPublicMCPReusingParam{}).Error
+	return tx.Model(&PublicMCPReusingParam{}).Where("mcp_id = ?", p.ID).Delete(&PublicMCPReusingParam{}).Error
+}
+
+func (p *PublicMCP) MarshalJSON() ([]byte, error) {
+	type Alias PublicMCP
+	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)
 }
 
 // CreatePublicMCP creates a new MCP
@@ -127,6 +189,10 @@ func UpdatePublicMCP(mcp *PublicMCP) error {
 	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).
@@ -163,7 +229,11 @@ func GetPublicMCPs(page int, perPage int, mcpType PublicMCPType, keyword string)
 
 	if keyword != "" {
 		keyword = "%" + keyword + "%"
-		tx = tx.Where("name LIKE ? OR author LIKE ? OR tags LIKE ?", 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)
+		}
 	}
 
 	err = tx.Count(&total).Error
@@ -185,19 +255,19 @@ func GetPublicMCPs(page int, perPage int, mcpType PublicMCPType, keyword string)
 	return mcps, total, err
 }
 
-func SaveGroupPublicMCPReusingParam(param *GroupPublicMCPReusingParam) (err error) {
+func SaveGroupPublicMCPReusingParam(param *PublicMCPReusingParam) (err error) {
 	return DB.Save(param).Error
 }
 
 // UpdateGroupPublicMCPReusingParam updates an existing GroupMCPReusingParam
-func UpdateGroupPublicMCPReusingParam(param *GroupPublicMCPReusingParam) error {
+func UpdateGroupPublicMCPReusingParam(param *PublicMCPReusingParam) error {
 	result := DB.
 		Select([]string{
 			"reusing_params",
 		}).
 		Where("mcp_id = ? AND group_id = ?", param.MCPID, param.GroupID).
 		Updates(param)
-	return HandleUpdateResult(result, ErrGroupMCPReusingParamNotFound)
+	return HandleUpdateResult(result, ErrMCPReusingParamNotFound)
 }
 
 // DeleteGroupPublicMCPReusingParam deletes a GroupMCPReusingParam
@@ -207,16 +277,16 @@ func DeleteGroupPublicMCPReusingParam(mcpID string, groupID string) error {
 	}
 	result := DB.
 		Where("mcp_id = ? AND group_id = ?", mcpID, groupID).
-		Delete(&GroupPublicMCPReusingParam{})
-	return HandleUpdateResult(result, ErrGroupMCPReusingParamNotFound)
+		Delete(&PublicMCPReusingParam{})
+	return HandleUpdateResult(result, ErrMCPReusingParamNotFound)
 }
 
 // GetGroupPublicMCPReusingParam retrieves a GroupMCPReusingParam by MCP ID and Group ID
-func GetGroupPublicMCPReusingParam(mcpID string, groupID string) (*GroupPublicMCPReusingParam, error) {
+func GetGroupPublicMCPReusingParam(mcpID string, groupID string) (*PublicMCPReusingParam, error) {
 	if mcpID == "" || groupID == "" {
 		return nil, errors.New("MCP ID or Group ID is empty")
 	}
-	var param GroupPublicMCPReusingParam
+	var param PublicMCPReusingParam
 	err := DB.Where("mcp_id = ? AND group_id = ?", mcpID, groupID).First(&param).Error
-	return &param, HandleNotFound(err, ErrGroupMCPReusingParamNotFound)
+	return &param, HandleNotFound(err, ErrMCPReusingParamNotFound)
 }

+ 1 - 1
core/relay/adaptor/ali/stt-realtime.go

@@ -94,7 +94,7 @@ func ConvertSTTRequest(meta *meta.Meta, request *http.Request) (string, http.Hea
 		Header: STTHeader{
 			Action:    "run-task",
 			Streaming: "duplex",
-			TaskID:    uuid.New().String(),
+			TaskID:    uuid.NewString(),
 		},
 		Payload: STTPayload{
 			Model:     meta.ActualModel,

+ 1 - 1
core/relay/adaptor/ali/tts.go

@@ -122,7 +122,7 @@ func ConvertTTSRequest(meta *meta.Meta, req *http.Request) (string, http.Header,
 		Header: TTSHeader{
 			Action:    "run-task",
 			Streaming: "out",
-			TaskID:    uuid.New().String(),
+			TaskID:    uuid.NewString(),
 		},
 		Payload: TTSPayload{
 			Model:     request.Model,

+ 1 - 1
core/relay/adaptor/doubaoaudio/tts.go

@@ -100,7 +100,7 @@ func ConvertTTSRequest(meta *meta.Meta, req *http.Request) (string, http.Header,
 			SpeedRatio: request.Speed,
 		},
 		Request: RequestConfig{
-			ReqID:     uuid.New().String(),
+			ReqID:     uuid.NewString(),
 			Text:      request.Input,
 			TextType:  textType,
 			Operation: "submit",

+ 3 - 15
core/relay/adaptor/openai/id.go

@@ -1,23 +1,11 @@
 package openai
 
-import (
-	"encoding/hex"
-
-	"github.com/google/uuid"
-	"github.com/labring/aiproxy/core/common/conv"
-)
-
-func shortUUID() string {
-	var buf [32]byte
-	bytes := uuid.New()
-	hex.Encode(buf[:], bytes[:])
-	return conv.BytesToString(buf[:])
-}
+import "github.com/labring/aiproxy/core/common"
 
 func ChatCompletionID() string {
-	return "chatcmpl-" + shortUUID()
+	return "chatcmpl-" + common.ShortUUID()
 }
 
 func CallID() string {
-	return "call_" + shortUUID()
+	return "call_" + common.ShortUUID()
 }

+ 9 - 1
core/router/api.go

@@ -182,7 +182,6 @@ func SetAPIRouter(router *gin.Engine) {
 			monitorRoute.GET("/models", controller.GetModelsErrorRate)
 			monitorRoute.GET("/banned_channels", controller.GetAllBannedModelChannels)
 		}
-
 		publicMcpRoute := apiRouter.Group("/mcp/public")
 		{
 			publicMcpRoute.GET("/", controller.GetPublicMCPs)
@@ -193,5 +192,14 @@ func SetAPIRouter(router *gin.Engine) {
 			publicMcpRoute.GET("/:id/group/:group/params", controller.GetGroupPublicMCPReusingParam)
 			publicMcpRoute.POST("/:id/group/:group/params", controller.SaveGroupPublicMCPReusingParam)
 		}
+
+		groupMcpRoute := apiRouter.Group("/mcp/group")
+		{
+			groupMcpRoute.GET("/:group", controller.GetGroupMCPs)
+			groupMcpRoute.GET("/:group/:id", controller.GetGroupMCPByID)
+			groupMcpRoute.POST("/:group", controller.CreateGroupMCP)
+			groupMcpRoute.PUT("/:group/:id", controller.UpdateGroupMCP)
+			groupMcpRoute.DELETE("/:group/:id", controller.DeleteGroupMCP)
+		}
 	}
 }

+ 5 - 2
core/router/mcp.go

@@ -9,6 +9,9 @@ import (
 func SetMCPRouter(router *gin.Engine) {
 	mcpRoute := router.Group("/mcp", middleware.MCPAuth)
 
-	mcpRoute.GET("/public/:id/sse", controller.MCPSseProxy)
-	mcpRoute.POST("/message", controller.MCPMessage)
+	mcpRoute.GET("/public/:id/sse", controller.PublicMCPSseServer)
+	mcpRoute.POST("/public/message", controller.PublicMCPMessage)
+
+	mcpRoute.GET("/group/:id/sse", controller.GroupMCPSseServer)
+	mcpRoute.POST("/group/message", controller.GroupMCPMessage)
 }

+ 6 - 0
go.work.sum

@@ -1,9 +1,11 @@
 cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
+cel.dev/expr v0.20.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
 cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA=
 cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q=
 cloud.google.com/go/longrunning v0.6.6/go.mod h1:hyeGJUrPHcx0u2Uu1UFSoYZLn4lkMrccJig0t4FI7yw=
 cloud.google.com/go/translate v1.10.3/go.mod h1:GW0vC1qvPtd3pgtypCv4k4U8B7EdgK9/QEF2aJEUovs=
 github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0/go.mod h1:obipzmGjfSjam60XLwGfqUkJsfiheAl+TUjG+4yzyPM=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0/go.mod h1:2bIszWvQRlJVmJLiuLhukLImRjKPcYdzzsx6darK02A=
 github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
 github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M=
@@ -15,6 +17,7 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.33.18/go.mod h1:cQnB8CUnxbMU82JvlqjK
 github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=
 github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
 github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
+github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA=
@@ -22,6 +25,7 @@ github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1
 github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
 github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
 github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
 github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/google/go-pkcs11 v0.3.0/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY=
@@ -43,9 +47,11 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV
 github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
 github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g=
 github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
+github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
 go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
 go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo=
 golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=

+ 29 - 12
openapi-mcp/convert/convert.go

@@ -21,6 +21,8 @@ type Options struct {
 	ServerName     string
 	Version        string
 	ToolNamePrefix string
+	ServerAddr     string
+	Authorization  string
 }
 
 // Converter represents an OpenAPI to MCP converter
@@ -61,16 +63,19 @@ func (c *Converter) Convert() (*server.MCPServer, error) {
 		c.options.Version,
 	)
 
-	servers := c.parser.GetServers()
-	defaultServer := ""
-	if len(servers) == 1 {
-		server, err := getServerURL(c.options.OpenAPIFrom, servers[0].URL)
-		if err != nil {
-			return nil, err
+	defaultServer := c.options.ServerAddr
+	// Use custom server address if provided
+	if defaultServer == "" {
+		servers := c.parser.GetServers()
+		if len(servers) == 1 {
+			server, err := getServerURL(c.options.OpenAPIFrom, servers[0].URL)
+			if err != nil {
+				return nil, err
+			}
+			defaultServer = server
+		} else if len(servers) == 0 {
+			defaultServer, _ = getServerURL(c.options.OpenAPIFrom, "")
 		}
-		defaultServer = server
-	} else if len(servers) == 0 {
-		defaultServer, _ = getServerURL(c.options.OpenAPIFrom, "")
 	}
 
 	// Process each path and operation
@@ -78,7 +83,7 @@ func (c *Converter) Convert() (*server.MCPServer, error) {
 		operations := getOperations(pathItem)
 		for method, operation := range operations {
 			tool := c.convertOperation(path, method, operation)
-			handler := newHandler(defaultServer, path, method, operation)
+			handler := newHandler(defaultServer, c.options.Authorization, path, method, operation)
 			mcpServer.AddTool(*tool, handler)
 		}
 	}
@@ -108,7 +113,7 @@ func getServerURL(from string, dir string) (string, error) {
 }
 
 // TODO: valid operation
-func newHandler(defaultServer string, path, method string, _ *openapi3.Operation) server.ToolHandlerFunc {
+func newHandler(defaultServer, authorization string, path, method string, _ *openapi3.Operation) server.ToolHandlerFunc {
 	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 		arg := getArgs(request.Params.Arguments)
 
@@ -173,6 +178,8 @@ func newHandler(defaultServer string, path, method string, _ *openapi3.Operation
 
 		// Add authentication if provided
 		switch {
+		case authorization != "":
+			httpReq.Header.Set("Authorization", authorization)
 		case arg.AuthToken != "":
 			httpReq.Header.Set("Authorization", "Bearer "+arg.AuthToken)
 		case arg.AuthUsername != "" && arg.AuthPassword != "":
@@ -319,6 +326,11 @@ func (c *Converter) convertOperation(path, method string, operation *openapi3.Op
 	// Add server address parameter
 	servers := c.parser.GetServers()
 	switch {
+	case c.options.ServerAddr != "":
+		// Use custom server address from options
+		args = append(args, mcp.WithString("openapi|server_addr",
+			mcp.Description("Server address to connect to"),
+			mcp.DefaultString(c.options.ServerAddr)))
 	case len(servers) == 0:
 		if c.options.OpenAPIFrom != "" {
 			u, err := getServerURL(c.options.OpenAPIFrom, "")
@@ -357,7 +369,12 @@ func (c *Converter) convertOperation(path, method string, operation *openapi3.Op
 	}
 
 	// Handle security requirements if present and enabled
-	if operation.Security != nil && len(*operation.Security) > 0 {
+	if c.options.Authorization != "" {
+		// Use custom authorization from options
+		args = append(args, mcp.WithString("header|Authorization",
+			mcp.Description("Authorization header"),
+			mcp.DefaultString(c.options.Authorization)))
+	} else if operation.Security != nil && len(*operation.Security) > 0 {
 		securityArgs := c.convertSecurityRequirements(*operation.Security)
 		args = append(args, securityArgs...)
 	}

+ 2 - 1
openapi-mcp/go.mod

@@ -4,7 +4,7 @@ go 1.23.8
 
 require (
 	github.com/getkin/kin-openapi v0.131.0
-	github.com/mark3labs/mcp-go v0.21.1
+	github.com/mark3labs/mcp-go v0.22.0
 )
 
 require (
@@ -18,6 +18,7 @@ require (
 	github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
 	github.com/perimeterx/marshmallow v1.1.5 // indirect
 	github.com/rogpeppe/go-internal v1.13.1 // indirect
+	github.com/spf13/cast v1.7.1 // indirect
 	github.com/ugorji/go/codec v1.2.12 // indirect
 	github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect

+ 8 - 2
openapi-mcp/go.sum

@@ -1,5 +1,7 @@
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
+github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
 github.com/getkin/kin-openapi v0.131.0 h1:NO2UeHnFKRYhZ8wg6Nyh5Cq7dHk4suQQr72a4pMrDxE=
 github.com/getkin/kin-openapi v0.131.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58=
 github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic=
@@ -8,6 +10,8 @@ github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZ
 github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
 github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
 github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
@@ -18,8 +22,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
 github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
-github.com/mark3labs/mcp-go v0.21.1 h1:7Ek6KPIIbMhEYHRiRIg6K6UAgNZCJaHKQp926MNr6V0=
-github.com/mark3labs/mcp-go v0.21.1/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE=
+github.com/mark3labs/mcp-go v0.22.0 h1:cCEBWi4Yy9Kio+OW1hWIyi4WLsSr+RBBK6FI5tj+b7I=
+github.com/mark3labs/mcp-go v0.22.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
 github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
@@ -32,6 +36,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
 github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
+github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
+github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
 github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
 github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=