|
|
@@ -1,1853 +0,0 @@
|
|
|
-File: core/controller/mcp/publicmcp.go
|
|
|
-```go
|
|
|
-package controller
|
|
|
-
|
|
|
-import (
|
|
|
- "fmt"
|
|
|
- "net/http"
|
|
|
- "strconv"
|
|
|
-
|
|
|
- "github.com/bytedance/sonic"
|
|
|
- "github.com/gin-gonic/gin"
|
|
|
- "github.com/labring/aiproxy/core/common/config"
|
|
|
- "github.com/labring/aiproxy/core/controller/utils"
|
|
|
- "github.com/labring/aiproxy/core/middleware"
|
|
|
- "github.com/labring/aiproxy/core/model"
|
|
|
-)
|
|
|
-
|
|
|
-type MCPEndpoint struct {
|
|
|
- Host string `json:"host"`
|
|
|
- SSE string `json:"sse"`
|
|
|
- StreamableHTTP string `json:"streamable_http"`
|
|
|
-}
|
|
|
-
|
|
|
-type PublicMCPResponse struct {
|
|
|
- model.PublicMCP
|
|
|
- Endpoints MCPEndpoint `json:"endpoints"`
|
|
|
-}
|
|
|
-
|
|
|
-func (mcp *PublicMCPResponse) MarshalJSON() ([]byte, error) {
|
|
|
- type Alias PublicMCPResponse
|
|
|
- a := &struct {
|
|
|
- *Alias
|
|
|
- CreatedAt int64 `json:"created_at"`
|
|
|
- UpdateAt int64 `json:"update_at"`
|
|
|
- }{
|
|
|
- Alias: (*Alias)(mcp),
|
|
|
- CreatedAt: mcp.CreatedAt.UnixMilli(),
|
|
|
- UpdateAt: mcp.UpdateAt.UnixMilli(),
|
|
|
- }
|
|
|
- return sonic.Marshal(a)
|
|
|
-}
|
|
|
-
|
|
|
-func NewPublicMCPResponse(host string, mcp model.PublicMCP) PublicMCPResponse {
|
|
|
- ep := MCPEndpoint{}
|
|
|
- switch mcp.Type {
|
|
|
- case model.PublicMCPTypeProxySSE,
|
|
|
- model.PublicMCPTypeProxyStreamable,
|
|
|
- model.PublicMCPTypeEmbed,
|
|
|
- model.PublicMCPTypeOpenAPI:
|
|
|
- publicMCPHost := config.GetPublicMCPHost()
|
|
|
- if publicMCPHost == "" {
|
|
|
- ep.Host = host
|
|
|
- ep.SSE = fmt.Sprintf("/mcp/public/%s/sse", mcp.ID)
|
|
|
- ep.StreamableHTTP = "/mcp/public/" + mcp.ID
|
|
|
- } else {
|
|
|
- ep.Host = fmt.Sprintf("%s.%s", mcp.ID, publicMCPHost)
|
|
|
- ep.SSE = "/sse"
|
|
|
- ep.StreamableHTTP = "/mcp"
|
|
|
- }
|
|
|
- case model.PublicMCPTypeDocs:
|
|
|
- }
|
|
|
- return PublicMCPResponse{
|
|
|
- PublicMCP: mcp,
|
|
|
- Endpoints: ep,
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-func NewPublicMCPResponses(host string, mcps []model.PublicMCP) []PublicMCPResponse {
|
|
|
- responses := make([]PublicMCPResponse, len(mcps))
|
|
|
- for i, mcp := range mcps {
|
|
|
- responses[i] = NewPublicMCPResponse(host, mcp)
|
|
|
- }
|
|
|
- return responses
|
|
|
-}
|
|
|
-
|
|
|
-// GetPublicMCPs godoc
|
|
|
-//
|
|
|
-// @Summary Get MCPs
|
|
|
-// @Description Get a list of MCPs with pagination and filtering
|
|
|
-// @Tags mcp
|
|
|
-// @Produce json
|
|
|
-// @Security ApiKeyAuth
|
|
|
-// @Param page query int false "Page number"
|
|
|
-// @Param per_page query int false "Items per page"
|
|
|
-// @Param type query string false "MCP type"
|
|
|
-// @Param keyword query string false "Search keyword"
|
|
|
-// @Param status query int false "MCP status"
|
|
|
-// @Success 200 {object} middleware.APIResponse{data=[]PublicMCPResponse}
|
|
|
-// @Router /api/mcp/public/ [get]
|
|
|
-func GetPublicMCPs(c *gin.Context) {
|
|
|
- page, perPage := utils.ParsePageParams(c)
|
|
|
- mcpType := model.PublicMCPType(c.Query("type"))
|
|
|
- keyword := c.Query("keyword")
|
|
|
- status, _ := strconv.Atoi(c.Query("status"))
|
|
|
-
|
|
|
- if status == 0 {
|
|
|
- status = int(model.PublicMCPStatusEnabled)
|
|
|
- }
|
|
|
-
|
|
|
- mcps, total, err := model.GetPublicMCPs(
|
|
|
- page,
|
|
|
- perPage,
|
|
|
- mcpType,
|
|
|
- keyword,
|
|
|
- model.PublicMCPStatus(status),
|
|
|
- )
|
|
|
- if err != nil {
|
|
|
- middleware.ErrorResponse(c, http.StatusInternalServerError, err.Error())
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- middleware.SuccessResponse(c, gin.H{
|
|
|
- "mcps": NewPublicMCPResponses(c.Request.Host, mcps),
|
|
|
- "total": total,
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-// GetAllPublicMCPs godoc
|
|
|
-//
|
|
|
-// @Summary Get all MCPs
|
|
|
-// @Description Get all MCPs with filtering
|
|
|
-// @Tags mcp
|
|
|
-// @Produce json
|
|
|
-// @Security ApiKeyAuth
|
|
|
-// @Param status query int false "MCP status"
|
|
|
-// @Success 200 {object} middleware.APIResponse{data=[]PublicMCPResponse}
|
|
|
-// @Router /api/mcp/public/all [get]
|
|
|
-func GetAllPublicMCPs(c *gin.Context) {
|
|
|
- status, _ := strconv.Atoi(c.Query("status"))
|
|
|
-
|
|
|
- if status == 0 {
|
|
|
- status = int(model.PublicMCPStatusEnabled)
|
|
|
- }
|
|
|
-
|
|
|
- mcps, err := model.GetAllPublicMCPs(model.PublicMCPStatus(status))
|
|
|
- if err != nil {
|
|
|
- middleware.ErrorResponse(c, http.StatusInternalServerError, err.Error())
|
|
|
- return
|
|
|
- }
|
|
|
- middleware.SuccessResponse(c, NewPublicMCPResponses(c.Request.Host, mcps))
|
|
|
-}
|
|
|
-
|
|
|
-// GetPublicMCPByIDHandler godoc
|
|
|
-//
|
|
|
-// @Summary Get MCP by ID
|
|
|
-// @Description Get a specific MCP by its ID
|
|
|
-// @Tags mcp
|
|
|
-// @Produce json
|
|
|
-// @Security ApiKeyAuth
|
|
|
-// @Param id path string true "MCP ID"
|
|
|
-// @Success 200 {object} middleware.APIResponse{data=PublicMCPResponse}
|
|
|
-// @Router /api/mcp/public/{id} [get]
|
|
|
-func GetPublicMCPByIDHandler(c *gin.Context) {
|
|
|
- id := c.Param("id")
|
|
|
- if id == "" {
|
|
|
- middleware.ErrorResponse(c, http.StatusBadRequest, "MCP ID is required")
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- mcp, err := model.GetPublicMCPByID(id)
|
|
|
- if err != nil {
|
|
|
- middleware.ErrorResponse(c, http.StatusNotFound, err.Error())
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- middleware.SuccessResponse(c, NewPublicMCPResponse(c.Request.Host, mcp))
|
|
|
-}
|
|
|
-
|
|
|
-// CreatePublicMCP godoc
|
|
|
-//
|
|
|
-// @Summary Create MCP
|
|
|
-// @Description Create a new MCP
|
|
|
-// @Tags mcp
|
|
|
-// @Accept json
|
|
|
-// @Produce json
|
|
|
-// @Security ApiKeyAuth
|
|
|
-// @Param mcp body model.PublicMCP true "MCP object"
|
|
|
-// @Success 200 {object} middleware.APIResponse{data=PublicMCPResponse}
|
|
|
-// @Router /api/mcp/public/ [post]
|
|
|
-func CreatePublicMCP(c *gin.Context) {
|
|
|
- var mcp model.PublicMCP
|
|
|
- if err := c.ShouldBindJSON(&mcp); err != nil {
|
|
|
- middleware.ErrorResponse(c, http.StatusBadRequest, err.Error())
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- if err := model.CreatePublicMCP(&mcp); err != nil {
|
|
|
- middleware.ErrorResponse(c, http.StatusInternalServerError, err.Error())
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- middleware.SuccessResponse(c, NewPublicMCPResponse(c.Request.Host, mcp))
|
|
|
-}
|
|
|
-
|
|
|
-type UpdatePublicMCPStatusRequest struct {
|
|
|
- Status model.PublicMCPStatus `json:"status"`
|
|
|
-}
|
|
|
-
|
|
|
-// UpdatePublicMCPStatus godoc
|
|
|
-//
|
|
|
-// @Summary Update MCP status
|
|
|
-// @Description Update the status of an MCP
|
|
|
-// @Tags mcp
|
|
|
-// @Accept json
|
|
|
-// @Produce json
|
|
|
-// @Security ApiKeyAuth
|
|
|
-// @Param id path string true "MCP ID"
|
|
|
-// @Param status body UpdatePublicMCPStatusRequest true "MCP status"
|
|
|
-// @Success 200 {object} middleware.APIResponse
|
|
|
-// @Router /api/mcp/public/{id}/status [post]
|
|
|
-func UpdatePublicMCPStatus(c *gin.Context) {
|
|
|
- id := c.Param("id")
|
|
|
- if id == "" {
|
|
|
- middleware.ErrorResponse(c, http.StatusBadRequest, "MCP ID is required")
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- var status UpdatePublicMCPStatusRequest
|
|
|
- if err := c.ShouldBindJSON(&status); err != nil {
|
|
|
- middleware.ErrorResponse(c, http.StatusBadRequest, err.Error())
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- if err := model.UpdatePublicMCPStatus(id, status.Status); err != nil {
|
|
|
- middleware.ErrorResponse(c, http.StatusInternalServerError, err.Error())
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- middleware.SuccessResponse(c, nil)
|
|
|
-}
|
|
|
-
|
|
|
-// UpdatePublicMCP godoc
|
|
|
-//
|
|
|
-// @Summary Update MCP
|
|
|
-// @Description Update an existing MCP
|
|
|
-// @Tags mcp
|
|
|
-// @Accept json
|
|
|
-// @Produce json
|
|
|
-// @Security ApiKeyAuth
|
|
|
-// @Param id path string true "MCP ID"
|
|
|
-// @Param mcp body model.PublicMCP true "MCP object"
|
|
|
-// @Success 200 {object} middleware.APIResponse{data=PublicMCPResponse}
|
|
|
-// @Router /api/mcp/public/{id} [put]
|
|
|
-func UpdatePublicMCP(c *gin.Context) {
|
|
|
- id := c.Param("id")
|
|
|
- if id == "" {
|
|
|
- middleware.ErrorResponse(c, http.StatusBadRequest, "MCP ID is required")
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- var mcp model.PublicMCP
|
|
|
- if err := c.ShouldBindJSON(&mcp); err != nil {
|
|
|
- middleware.ErrorResponse(c, http.StatusBadRequest, err.Error())
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- mcp.ID = id
|
|
|
-
|
|
|
- if err := model.UpdatePublicMCP(&mcp); err != nil {
|
|
|
- middleware.ErrorResponse(c, http.StatusInternalServerError, err.Error())
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- middleware.SuccessResponse(c, NewPublicMCPResponse(c.Request.Host, mcp))
|
|
|
-}
|
|
|
-
|
|
|
-// DeletePublicMCP godoc
|
|
|
-//
|
|
|
-// @Summary Delete MCP
|
|
|
-// @Description Delete an MCP by ID
|
|
|
-// @Tags mcp
|
|
|
-// @Produce json
|
|
|
-// @Security ApiKeyAuth
|
|
|
-// @Param id path string true "MCP ID"
|
|
|
-// @Success 200 {object} middleware.APIResponse
|
|
|
-// @Router /api/mcp/public/{id} [delete]
|
|
|
-func DeletePublicMCP(c *gin.Context) {
|
|
|
- id := c.Param("id")
|
|
|
- if id == "" {
|
|
|
- middleware.ErrorResponse(c, http.StatusBadRequest, "MCP ID is required")
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- if err := model.DeletePublicMCP(id); err != nil {
|
|
|
- middleware.ErrorResponse(c, http.StatusInternalServerError, err.Error())
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- middleware.SuccessResponse(c, nil)
|
|
|
-}
|
|
|
-
|
|
|
-// GetGroupPublicMCPReusingParam godoc
|
|
|
-//
|
|
|
-// @Summary Get group MCP reusing parameters
|
|
|
-// @Description Get reusing parameters for a specific group and MCP
|
|
|
-// @Tags mcp
|
|
|
-// @Produce json
|
|
|
-// @Security ApiKeyAuth
|
|
|
-// @Param id path string true "MCP ID"
|
|
|
-// @Param group path string true "Group ID"
|
|
|
-// @Success 200 {object} middleware.APIResponse{data=model.PublicMCPReusingParam}
|
|
|
-// @Router /api/mcp/public/{id}/group/{group}/params [get]
|
|
|
-func GetGroupPublicMCPReusingParam(c *gin.Context) {
|
|
|
- mcpID := c.Param("id")
|
|
|
- groupID := c.Param("group")
|
|
|
-
|
|
|
- if mcpID == "" || groupID == "" {
|
|
|
- middleware.ErrorResponse(c, http.StatusBadRequest, "MCP ID and Group ID are required")
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- param, err := model.GetPublicMCPReusingParam(mcpID, groupID)
|
|
|
- if err != nil {
|
|
|
- middleware.ErrorResponse(c, http.StatusNotFound, err.Error())
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- middleware.SuccessResponse(c, param)
|
|
|
-}
|
|
|
-
|
|
|
-// SaveGroupPublicMCPReusingParam godoc
|
|
|
-//
|
|
|
-// @Summary Create or update group MCP reusing parameters
|
|
|
-// @Description Create or update reusing parameters for a specific group and MCP
|
|
|
-// @Tags mcp
|
|
|
-// @Accept json
|
|
|
-// @Produce json
|
|
|
-// @Security ApiKeyAuth
|
|
|
-// @Param id path string true "MCP ID"
|
|
|
-// @Param group path string true "Group ID"
|
|
|
-// @Param params body model.PublicMCPReusingParam true "Reusing parameters"
|
|
|
-// @Success 200 {object} middleware.APIResponse
|
|
|
-// @Router /api/mcp/public/{id}/group/{group}/params [post]
|
|
|
-func SaveGroupPublicMCPReusingParam(c *gin.Context) {
|
|
|
- mcpID := c.Param("id")
|
|
|
- groupID := c.Param("group")
|
|
|
-
|
|
|
- if mcpID == "" || groupID == "" {
|
|
|
- middleware.ErrorResponse(c, http.StatusBadRequest, "MCP ID and Group ID are required")
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- var param model.PublicMCPReusingParam
|
|
|
- if err := c.ShouldBindJSON(¶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后端等操作
|