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