feat: mcp server

This commit is contained in:
Jacky 2025-04-29 17:15:42 +08:00
parent c4a9d03bb3
commit e8ee931e16
No known key found for this signature in database
GPG key ID: 215C21B10DF38B4D
51 changed files with 2749 additions and 1526 deletions

View file

@ -1,7 +1,11 @@
package config
import (
"os"
"sort"
"github.com/0xJacky/Nginx-UI/internal/nginx"
"github.com/uozi-tech/cosy/logger"
)
type ConfigsSort struct {
@ -59,3 +63,59 @@ func Sort(key string, order string, configs []Config) []Config {
return configsSort.ConfigList
}
func GetConfigList(relativePath string, filter func(file os.FileInfo) bool) ([]Config, error) {
configFiles, err := os.ReadDir(nginx.GetConfPath(relativePath))
if err != nil {
return nil, err
}
configs := make([]Config, 0)
for i := range configFiles {
file := configFiles[i]
fileInfo, err := file.Info()
if err != nil {
logger.Error("Get File Info Error", file.Name(), err)
continue
}
if filter != nil && !filter(fileInfo) {
continue
}
switch mode := fileInfo.Mode(); {
case mode.IsRegular(): // regular file, not a hidden file
if "." == file.Name()[0:1] {
continue
}
case mode&os.ModeSymlink != 0: // is a symbol
var targetPath string
targetPath, err = os.Readlink(nginx.GetConfPath(relativePath, file.Name()))
if err != nil {
logger.Error("Read Symlink Error", targetPath, err)
continue
}
var targetInfo os.FileInfo
targetInfo, err = os.Stat(targetPath)
if err != nil {
logger.Error("Stat Error", targetPath, err)
continue
}
// hide the file if it's target file is a directory
if targetInfo.IsDir() {
continue
}
}
configs = append(configs, Config{
Name: file.Name(),
ModifiedAt: fileInfo.ModTime(),
Size: fileInfo.Size(),
IsDir: fileInfo.IsDir(),
})
}
return configs, nil
}

View file

@ -340,7 +340,6 @@ func UpgradeStepThree() error {
// Get old container name from environment variable, fallback to settings if not available
currentContainerName := os.Getenv("NGINX_UI_CONTAINER_NAME")
if currentContainerName == "" {
logger.Warn("Old container name not found in environment, skipping cleanup")
return nil
}
oldContainerName := currentContainerName + OldSuffix

View file

@ -13,6 +13,7 @@ import (
"github.com/0xJacky/Nginx-UI/internal/cluster"
"github.com/0xJacky/Nginx-UI/internal/cron"
"github.com/0xJacky/Nginx-UI/internal/docker"
"github.com/0xJacky/Nginx-UI/internal/mcp"
"github.com/0xJacky/Nginx-UI/internal/passkey"
"github.com/0xJacky/Nginx-UI/internal/validation"
"github.com/0xJacky/Nginx-UI/model"
@ -61,6 +62,7 @@ func InitAfterDatabase() {
analytic.RetrieveNodesStatus,
passkey.Init,
RegisterAcmeUser,
mcp.Init,
}
for _, v := range syncs {
@ -138,7 +140,5 @@ func CheckAndCleanupOTAContainers() {
err := docker.UpgradeStepThree()
if err != nil {
logger.Error("Failed to cleanup OTA containers:", err)
} else {
logger.Info("OTA container cleanup completed successfully")
}
}

60
internal/mcp/server.go Normal file
View file

@ -0,0 +1,60 @@
package mcp
import (
"context"
"sync"
"github.com/gin-gonic/gin"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
var (
mcpServer = server.NewMCPServer(
"Nginx",
"1.0.0",
server.WithResourceCapabilities(true, true),
server.WithLogging(),
server.WithRecovery(),
)
sseServer = server.NewSSEServer(
mcpServer,
server.WithSSEEndpoint("/mcp"),
server.WithMessageEndpoint("/mcp_message"),
)
)
const (
MimeTypeJSON = "application/json"
MimeTypeText = "text/plain"
)
type Resource struct {
Resource mcp.Resource
Handler func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error)
}
type Tool struct {
Tool mcp.Tool
Handler func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error)
}
var (
tools = []Tool{}
toolMutex sync.Mutex
)
func AddTool(tool mcp.Tool, handler func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error)) {
toolMutex.Lock()
defer toolMutex.Unlock()
tools = append(tools, Tool{Tool: tool, Handler: handler})
}
func ServeHTTP(c *gin.Context) {
sseServer.ServeHTTP(c.Writer, c.Request)
}
func Init() {
for _, tool := range tools {
mcpServer.AddTool(tool.Tool, tool.Handler)
}
}

View file

@ -59,6 +59,12 @@ func AuthRequired() gin.HandlerFunc {
return
}
if token := c.Query("node_secret"); token != "" && token == settings.NodeSettings.Secret {
c.Set("Secret", token)
c.Next()
return
}
token := getToken(c)
if token == "" {
abortWithAuthFailure()