fix(mcp): prevent panic on nil interface conversion for tool arguments

Add safe argument extraction helper functions in internal/mcp/args.go
that handle nil values gracefully instead of panicking on direct type
assertions.

This fixes the issue where MCP config tools panic with:
'interface conversion: interface {} is nil, not string'
when called via Claude Desktop with protocol version 2025-11-25.

Affected tools:
- nginx_config_list
- nginx_config_get
- nginx_config_add
- nginx_config_modify
- nginx_config_rename
- nginx_config_mkdir
- nginx_config_history
- nginx_config_enable

Fixes #36ec

Co-authored-by: Jacky <me@jackyu.cn>
This commit is contained in:
Cursor Agent
2026-04-04 02:09:42 +00:00
parent 9f1b9bbbba
commit cd74b3bf4b
9 changed files with 136 additions and 100 deletions

34
internal/mcp/args.go Normal file
View File

@@ -0,0 +1,34 @@
package mcp
// GetString safely extracts a string value from the arguments map.
// Returns an empty string if the key doesn't exist or the value is nil.
func GetString(args map[string]interface{}, key string) string {
if v, ok := args[key]; ok && v != nil {
if s, ok := v.(string); ok {
return s
}
}
return ""
}
// GetBool safely extracts a boolean value from the arguments map.
// Returns false if the key doesn't exist or the value is nil.
func GetBool(args map[string]interface{}, key string) bool {
if v, ok := args[key]; ok && v != nil {
if b, ok := v.(bool); ok {
return b
}
}
return false
}
// GetSlice safely extracts a slice of interface{} from the arguments map.
// Returns nil if the key doesn't exist or the value is nil.
func GetSlice(args map[string]interface{}, key string) []interface{} {
if v, ok := args[key]; ok && v != nil {
if s, ok := v.([]interface{}); ok {
return s
}
}
return nil
}

View File

@@ -8,10 +8,11 @@ import (
"github.com/0xJacky/Nginx-UI/internal/config"
"github.com/0xJacky/Nginx-UI/internal/helper"
"github.com/0xJacky/Nginx-UI/internal/mcp"
"github.com/0xJacky/Nginx-UI/internal/nginx"
"github.com/0xJacky/Nginx-UI/model"
"github.com/0xJacky/Nginx-UI/query"
"github.com/mark3labs/mcp-go/mcp"
mcpgo "github.com/mark3labs/mcp-go/mcp"
)
const nginxConfigAddToolName = "nginx_config_add"
@@ -19,31 +20,29 @@ const nginxConfigAddToolName = "nginx_config_add"
// ErrFileAlreadyExists is returned when trying to create a file that already exists
var ErrFileAlreadyExists = errors.New("file already exists")
var nginxConfigAddTool = mcp.NewTool(
var nginxConfigAddTool = mcpgo.NewTool(
nginxConfigAddToolName,
mcp.WithDescription("Add or create a new Nginx configuration file"),
mcp.WithString("name", mcp.Description("The name of the configuration file to create")),
mcp.WithString("content", mcp.Description("The content of the configuration file")),
mcp.WithString("base_dir", mcp.Description("The base directory for the configuration")),
mcp.WithBoolean("overwrite", mcp.Description("Whether to overwrite an existing file")),
mcp.WithArray("sync_node_ids", mcp.Description("IDs of nodes to sync the configuration to")),
mcpgo.WithDescription("Add or create a new Nginx configuration file"),
mcpgo.WithString("name", mcpgo.Description("The name of the configuration file to create")),
mcpgo.WithString("content", mcpgo.Description("The content of the configuration file")),
mcpgo.WithString("base_dir", mcpgo.Description("The base directory for the configuration")),
mcpgo.WithBoolean("overwrite", mcpgo.Description("Whether to overwrite an existing file")),
mcpgo.WithArray("sync_node_ids", mcpgo.Description("IDs of nodes to sync the configuration to")),
)
func handleNginxConfigAdd(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
func handleNginxConfigAdd(ctx context.Context, request mcpgo.CallToolRequest) (*mcpgo.CallToolResult, error) {
args := request.GetArguments()
name := args["name"].(string)
content := args["content"].(string)
baseDir := args["base_dir"].(string)
overwrite := args["overwrite"].(bool)
name := mcp.GetString(args, "name")
content := mcp.GetString(args, "content")
baseDir := mcp.GetString(args, "base_dir")
overwrite := mcp.GetBool(args, "overwrite")
// Convert sync_node_ids from []interface{} to []uint64
syncNodeIdsInterface, ok := args["sync_node_ids"].([]interface{})
syncNodeIdsInterface := mcp.GetSlice(args, "sync_node_ids")
syncNodeIds := make([]uint64, 0)
if ok {
for _, id := range syncNodeIdsInterface {
if idFloat, ok := id.(float64); ok {
syncNodeIds = append(syncNodeIds, uint64(idFloat))
}
for _, id := range syncNodeIdsInterface {
if idFloat, ok := id.(float64); ok {
syncNodeIds = append(syncNodeIds, uint64(idFloat))
}
}
@@ -109,5 +108,5 @@ func handleNginxConfigAdd(ctx context.Context, request mcp.CallToolRequest) (*mc
}
jsonResult, _ := json.Marshal(result)
return mcp.NewToolResultText(string(jsonResult)), nil
return mcpgo.NewToolResultText(string(jsonResult)), nil
}

View File

@@ -9,25 +9,26 @@ import (
"github.com/0xJacky/Nginx-UI/internal/config"
"github.com/0xJacky/Nginx-UI/internal/helper"
"github.com/0xJacky/Nginx-UI/internal/mcp"
"github.com/0xJacky/Nginx-UI/internal/nginx"
"github.com/mark3labs/mcp-go/mcp"
mcpgo "github.com/mark3labs/mcp-go/mcp"
)
const nginxConfigEnableToolName = "nginx_config_enable"
var nginxConfigEnableTool = mcp.NewTool(
var nginxConfigEnableTool = mcpgo.NewTool(
nginxConfigEnableToolName,
mcp.WithDescription("Enable a previously created Nginx configuration (creates symlink in sites-enabled)"),
mcp.WithString("name", mcp.Description("The name of the configuration file to enable")),
mcp.WithString("base_dir", mcp.Description("The source directory (default: sites-available)")),
mcp.WithBoolean("overwrite", mcp.Description("Whether to overwrite an existing enabled configuration")),
mcpgo.WithDescription("Enable a previously created Nginx configuration (creates symlink in sites-enabled)"),
mcpgo.WithString("name", mcpgo.Description("The name of the configuration file to enable")),
mcpgo.WithString("base_dir", mcpgo.Description("The source directory (default: sites-available)")),
mcpgo.WithBoolean("overwrite", mcpgo.Description("Whether to overwrite an existing enabled configuration")),
)
func handleNginxConfigEnable(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
func handleNginxConfigEnable(ctx context.Context, request mcpgo.CallToolRequest) (*mcpgo.CallToolResult, error) {
args := request.GetArguments()
name := args["name"].(string)
baseDir := args["base_dir"].(string)
overwrite := args["overwrite"].(bool)
name := mcp.GetString(args, "name")
baseDir := mcp.GetString(args, "base_dir")
overwrite := mcp.GetBool(args, "overwrite")
if name == "" {
return nil, fmt.Errorf("argument 'name' is required")
@@ -110,6 +111,6 @@ func handleNginxConfigEnable(ctx context.Context, request mcp.CallToolRequest) (
}
jsonResult, _ := json.Marshal(result)
return mcp.NewToolResultText(string(jsonResult)), nil
return mcpgo.NewToolResultText(string(jsonResult)), nil
}

View File

@@ -7,21 +7,22 @@ import (
"path/filepath"
"github.com/0xJacky/Nginx-UI/internal/config"
"github.com/0xJacky/Nginx-UI/internal/mcp"
"github.com/0xJacky/Nginx-UI/query"
"github.com/mark3labs/mcp-go/mcp"
mcpgo "github.com/mark3labs/mcp-go/mcp"
)
const nginxConfigGetToolName = "nginx_config_get"
var nginxConfigGetTool = mcp.NewTool(
var nginxConfigGetTool = mcpgo.NewTool(
nginxConfigGetToolName,
mcp.WithDescription("Get a specific Nginx configuration file"),
mcp.WithString("relative_path", mcp.Description("The relative path to the configuration file")),
mcpgo.WithDescription("Get a specific Nginx configuration file"),
mcpgo.WithString("relative_path", mcpgo.Description("The relative path to the configuration file")),
)
func handleNginxConfigGet(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
func handleNginxConfigGet(ctx context.Context, request mcpgo.CallToolRequest) (*mcpgo.CallToolResult, error) {
args := request.GetArguments()
relativePath := args["relative_path"].(string)
relativePath := mcp.GetString(args, "relative_path")
absPath, err := config.ResolveAbsoluteOrRelativeConfPath(relativePath)
if err != nil {
@@ -55,5 +56,5 @@ func handleNginxConfigGet(ctx context.Context, request mcp.CallToolRequest) (*mc
}
jsonResult, _ := json.Marshal(result)
return mcp.NewToolResultText(string(jsonResult)), nil
return mcpgo.NewToolResultText(string(jsonResult)), nil
}

View File

@@ -4,21 +4,22 @@ import (
"context"
"encoding/json"
"github.com/0xJacky/Nginx-UI/internal/mcp"
"github.com/0xJacky/Nginx-UI/query"
"github.com/mark3labs/mcp-go/mcp"
mcpgo "github.com/mark3labs/mcp-go/mcp"
)
const nginxConfigHistoryToolName = "nginx_config_history"
var nginxConfigHistoryTool = mcp.NewTool(
var nginxConfigHistoryTool = mcpgo.NewTool(
nginxConfigHistoryToolName,
mcp.WithDescription("Get history of Nginx configuration changes"),
mcp.WithString("filepath", mcp.Description("The file path to get history for")),
mcpgo.WithDescription("Get history of Nginx configuration changes"),
mcpgo.WithString("filepath", mcpgo.Description("The file path to get history for")),
)
func handleNginxConfigHistory(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
func handleNginxConfigHistory(ctx context.Context, request mcpgo.CallToolRequest) (*mcpgo.CallToolResult, error) {
args := request.GetArguments()
filepath := args["filepath"].(string)
filepath := mcp.GetString(args, "filepath")
q := query.ConfigBackup
var histories, err = q.Where(q.FilePath.Eq(filepath)).Order(q.ID.Desc()).Find()
@@ -27,5 +28,5 @@ func handleNginxConfigHistory(ctx context.Context, request mcp.CallToolRequest)
}
jsonResult, _ := json.Marshal(histories)
return mcp.NewToolResultText(string(jsonResult)), nil
return mcpgo.NewToolResultText(string(jsonResult)), nil
}

View File

@@ -7,27 +7,28 @@ import (
"strings"
"github.com/0xJacky/Nginx-UI/internal/config"
"github.com/mark3labs/mcp-go/mcp"
"github.com/0xJacky/Nginx-UI/internal/mcp"
mcpgo "github.com/mark3labs/mcp-go/mcp"
)
const nginxConfigListToolName = "nginx_config_list"
var nginxConfigListTool = mcp.NewTool(
var nginxConfigListTool = mcpgo.NewTool(
nginxConfigListToolName,
mcp.WithDescription("This is the list of Nginx configurations"),
mcp.WithString("relative_path", mcp.Description("The relative path to the Nginx configurations")),
mcp.WithString("filter_by_name", mcp.Description("Filter the Nginx configurations by name")),
mcpgo.WithDescription("This is the list of Nginx configurations"),
mcpgo.WithString("relative_path", mcpgo.Description("The relative path to the Nginx configurations")),
mcpgo.WithString("filter_by_name", mcpgo.Description("Filter the Nginx configurations by name")),
)
func handleNginxConfigList(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
func handleNginxConfigList(ctx context.Context, request mcpgo.CallToolRequest) (*mcpgo.CallToolResult, error) {
args := request.GetArguments()
relativePath := args["relative_path"].(string)
filterByName := args["filter_by_name"].(string)
relativePath := mcp.GetString(args, "relative_path")
filterByName := mcp.GetString(args, "filter_by_name")
configs, err := config.GetConfigList(relativePath, func(file os.FileInfo) bool {
return filterByName == "" || strings.Contains(file.Name(), filterByName)
})
jsonResult, _ := json.Marshal(configs)
return mcp.NewToolResultText(string(jsonResult)), err
return mcpgo.NewToolResultText(string(jsonResult)), err
}

View File

@@ -6,22 +6,23 @@ import (
"os"
"github.com/0xJacky/Nginx-UI/internal/config"
"github.com/mark3labs/mcp-go/mcp"
"github.com/0xJacky/Nginx-UI/internal/mcp"
mcpgo "github.com/mark3labs/mcp-go/mcp"
)
const nginxConfigMkdirToolName = "nginx_config_mkdir"
var nginxConfigMkdirTool = mcp.NewTool(
var nginxConfigMkdirTool = mcpgo.NewTool(
nginxConfigMkdirToolName,
mcp.WithDescription("Create a new directory in the Nginx configuration path"),
mcp.WithString("base_path", mcp.Description("The base path where to create the directory")),
mcp.WithString("folder_name", mcp.Description("The name of the folder to create")),
mcpgo.WithDescription("Create a new directory in the Nginx configuration path"),
mcpgo.WithString("base_path", mcpgo.Description("The base path where to create the directory")),
mcpgo.WithString("folder_name", mcpgo.Description("The name of the folder to create")),
)
func handleNginxConfigMkdir(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
func handleNginxConfigMkdir(ctx context.Context, request mcpgo.CallToolRequest) (*mcpgo.CallToolResult, error) {
args := request.GetArguments()
basePath := args["base_path"].(string)
folderName := args["folder_name"].(string)
basePath := mcp.GetString(args, "base_path")
folderName := mcp.GetString(args, "folder_name")
fullPath, err := config.ResolveConfPath(basePath, folderName)
if err != nil {
@@ -39,5 +40,5 @@ func handleNginxConfigMkdir(ctx context.Context, request mcp.CallToolRequest) (*
}
jsonResult, _ := json.Marshal(result)
return mcp.NewToolResultText(string(jsonResult)), nil
return mcpgo.NewToolResultText(string(jsonResult)), nil
}

View File

@@ -8,9 +8,10 @@ import (
"github.com/0xJacky/Nginx-UI/internal/config"
"github.com/0xJacky/Nginx-UI/internal/helper"
"github.com/0xJacky/Nginx-UI/internal/mcp"
"github.com/0xJacky/Nginx-UI/model"
"github.com/0xJacky/Nginx-UI/query"
"github.com/mark3labs/mcp-go/mcp"
mcpgo "github.com/mark3labs/mcp-go/mcp"
"gorm.io/gen/field"
)
@@ -19,29 +20,27 @@ const nginxConfigModifyToolName = "nginx_config_modify"
// ErrFileNotFound is returned when a file is not found
var ErrFileNotFound = errors.New("file not found")
var nginxConfigModifyTool = mcp.NewTool(
var nginxConfigModifyTool = mcpgo.NewTool(
nginxConfigModifyToolName,
mcp.WithDescription("Modify an existing Nginx configuration file"),
mcp.WithString("relative_path", mcp.Description("The relative path to the configuration file")),
mcp.WithString("content", mcp.Description("The new content of the configuration file")),
mcp.WithBoolean("sync_overwrite", mcp.Description("Whether to overwrite existing files when syncing")),
mcp.WithArray("sync_node_ids", mcp.Description("IDs of nodes to sync the configuration to")),
mcpgo.WithDescription("Modify an existing Nginx configuration file"),
mcpgo.WithString("relative_path", mcpgo.Description("The relative path to the configuration file")),
mcpgo.WithString("content", mcpgo.Description("The new content of the configuration file")),
mcpgo.WithBoolean("sync_overwrite", mcpgo.Description("Whether to overwrite existing files when syncing")),
mcpgo.WithArray("sync_node_ids", mcpgo.Description("IDs of nodes to sync the configuration to")),
)
func handleNginxConfigModify(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
func handleNginxConfigModify(ctx context.Context, request mcpgo.CallToolRequest) (*mcpgo.CallToolResult, error) {
args := request.GetArguments()
relativePath := args["relative_path"].(string)
content := args["content"].(string)
syncOverwrite := args["sync_overwrite"].(bool)
relativePath := mcp.GetString(args, "relative_path")
content := mcp.GetString(args, "content")
syncOverwrite := mcp.GetBool(args, "sync_overwrite")
// Convert sync_node_ids from []interface{} to []uint64
syncNodeIdsInterface, ok := args["sync_node_ids"].([]interface{})
syncNodeIdsInterface := mcp.GetSlice(args, "sync_node_ids")
syncNodeIds := make([]uint64, 0)
if ok {
for _, id := range syncNodeIdsInterface {
if idFloat, ok := id.(float64); ok {
syncNodeIds = append(syncNodeIds, uint64(idFloat))
}
for _, id := range syncNodeIdsInterface {
if idFloat, ok := id.(float64); ok {
syncNodeIds = append(syncNodeIds, uint64(idFloat))
}
}
@@ -92,5 +91,5 @@ func handleNginxConfigModify(ctx context.Context, request mcp.CallToolRequest) (
}
jsonResult, _ := json.Marshal(result)
return mcp.NewToolResultText(string(jsonResult)), nil
return mcpgo.NewToolResultText(string(jsonResult)), nil
}

View File

@@ -9,36 +9,35 @@ import (
"github.com/0xJacky/Nginx-UI/internal/config"
"github.com/0xJacky/Nginx-UI/internal/helper"
"github.com/0xJacky/Nginx-UI/internal/mcp"
"github.com/0xJacky/Nginx-UI/model"
"github.com/0xJacky/Nginx-UI/query"
"github.com/mark3labs/mcp-go/mcp"
mcpgo "github.com/mark3labs/mcp-go/mcp"
)
const nginxConfigRenameToolName = "nginx_config_rename"
var nginxConfigRenameTool = mcp.NewTool(
var nginxConfigRenameTool = mcpgo.NewTool(
nginxConfigRenameToolName,
mcp.WithDescription("Rename a file or directory in the Nginx configuration path"),
mcp.WithString("base_path", mcp.Description("The base path where the file or directory is located")),
mcp.WithString("orig_name", mcp.Description("The original name of the file or directory")),
mcp.WithString("new_name", mcp.Description("The new name for the file or directory")),
mcp.WithArray("sync_node_ids", mcp.Description("IDs of nodes to sync the rename operation to")),
mcpgo.WithDescription("Rename a file or directory in the Nginx configuration path"),
mcpgo.WithString("base_path", mcpgo.Description("The base path where the file or directory is located")),
mcpgo.WithString("orig_name", mcpgo.Description("The original name of the file or directory")),
mcpgo.WithString("new_name", mcpgo.Description("The new name for the file or directory")),
mcpgo.WithArray("sync_node_ids", mcpgo.Description("IDs of nodes to sync the rename operation to")),
)
func handleNginxConfigRename(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
func handleNginxConfigRename(ctx context.Context, request mcpgo.CallToolRequest) (*mcpgo.CallToolResult, error) {
args := request.GetArguments()
basePath := args["base_path"].(string)
origName := args["orig_name"].(string)
newName := args["new_name"].(string)
basePath := mcp.GetString(args, "base_path")
origName := mcp.GetString(args, "orig_name")
newName := mcp.GetString(args, "new_name")
// Convert sync_node_ids from []interface{} to []uint64
syncNodeIdsInterface, ok := args["sync_node_ids"].([]interface{})
syncNodeIdsInterface := mcp.GetSlice(args, "sync_node_ids")
syncNodeIds := make([]uint64, 0)
if ok {
for _, id := range syncNodeIdsInterface {
if idFloat, ok := id.(float64); ok {
syncNodeIds = append(syncNodeIds, uint64(idFloat))
}
for _, id := range syncNodeIdsInterface {
if idFloat, ok := id.(float64); ok {
syncNodeIds = append(syncNodeIds, uint64(idFloat))
}
}
@@ -47,7 +46,7 @@ func handleNginxConfigRename(ctx context.Context, request mcp.CallToolRequest) (
"message": "No changes needed, names are identical",
}
jsonResult, _ := json.Marshal(result)
return mcp.NewToolResultText(string(jsonResult)), nil
return mcpgo.NewToolResultText(string(jsonResult)), nil
}
origFullPath, err := config.ResolveConfPath(basePath, origName)
@@ -118,5 +117,5 @@ func handleNginxConfigRename(ctx context.Context, request mcp.CallToolRequest) (
}
jsonResult, _ := json.Marshal(result)
return mcp.NewToolResultText(string(jsonResult)), nil
return mcpgo.NewToolResultText(string(jsonResult)), nil
}