fix(mcp): prevent panic on nil interface conversion for tool arguments#1622
Merged
fix(mcp): prevent panic on nil interface conversion for tool arguments#1622
Conversation
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>
…ta loss The safe argument extraction helpers (mcp.GetString, etc.) return zero values for nil/missing arguments, which could cause silent data loss. This adds explicit validation for required arguments in: - config_modify: validate relative_path and content - config_add: validate name and content - config_rename: validate orig_name and new_name - config_mkdir: validate folder_name This follows the same pattern already used in handleNginxConfigEnable.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Missing required argument validation in
config_get- Added empty string validation for
relative_pathin config_get andfilepathin config_history handlers, consistent with other handlers in the codebase.
- Added empty string validation for
Preview (376f14a479)
diff --git a/internal/mcp/args.go b/internal/mcp/args.go
new file mode 100644
--- /dev/null
+++ b/internal/mcp/args.go
@@ -1,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
+}
diff --git a/mcp/config/config_add.go b/mcp/config/config_add.go
--- a/mcp/config/config_add.go
+++ b/mcp/config/config_add.go
@@ -4,14 +4,16 @@
"context"
"encoding/json"
"errors"
+ "fmt"
"os"
"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 +21,36 @@
// 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")
+ if name == "" {
+ return nil, fmt.Errorf("argument 'name' is required")
+ }
+ if _, exists := args["content"]; !exists || args["content"] == nil {
+ return nil, fmt.Errorf("argument 'content' is required")
+ }
+
// 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 +116,5 @@
}
jsonResult, _ := json.Marshal(result)
- return mcp.NewToolResultText(string(jsonResult)), nil
+ return mcpgo.NewToolResultText(string(jsonResult)), nil
}
diff --git a/mcp/config/config_enable.go b/mcp/config/config_enable.go
--- a/mcp/config/config_enable.go
+++ b/mcp/config/config_enable.go
@@ -9,25 +9,26 @@
"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 @@
}
jsonResult, _ := json.Marshal(result)
- return mcp.NewToolResultText(string(jsonResult)), nil
+ return mcpgo.NewToolResultText(string(jsonResult)), nil
}
diff --git a/mcp/config/config_get.go b/mcp/config/config_get.go
--- a/mcp/config/config_get.go
+++ b/mcp/config/config_get.go
@@ -3,26 +3,32 @@
import (
"context"
"encoding/json"
+ "fmt"
"os"
"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")
+ if relativePath == "" {
+ return nil, fmt.Errorf("argument 'relative_path' is required")
+ }
+
absPath, err := config.ResolveAbsoluteOrRelativeConfPath(relativePath)
if err != nil {
return nil, err
@@ -55,5 +61,5 @@
}
jsonResult, _ := json.Marshal(result)
- return mcp.NewToolResultText(string(jsonResult)), nil
+ return mcpgo.NewToolResultText(string(jsonResult)), nil
}
diff --git a/mcp/config/config_history.go b/mcp/config/config_history.go
--- a/mcp/config/config_history.go
+++ b/mcp/config/config_history.go
@@ -3,23 +3,29 @@
import (
"context"
"encoding/json"
+ "fmt"
+ "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")
+ if filepath == "" {
+ return nil, fmt.Errorf("argument 'filepath' is required")
+ }
+
q := query.ConfigBackup
var histories, err = q.Where(q.FilePath.Eq(filepath)).Order(q.ID.Desc()).Find()
if err != nil {
@@ -27,5 +33,5 @@
}
jsonResult, _ := json.Marshal(histories)
- return mcp.NewToolResultText(string(jsonResult)), nil
+ return mcpgo.NewToolResultText(string(jsonResult)), nil
}
diff --git a/mcp/config/config_list.go b/mcp/config/config_list.go
--- a/mcp/config/config_list.go
+++ b/mcp/config/config_list.go
@@ -7,27 +7,28 @@
"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
}
diff --git a/mcp/config/config_mkdir.go b/mcp/config/config_mkdir.go
--- a/mcp/config/config_mkdir.go
+++ b/mcp/config/config_mkdir.go
@@ -3,26 +3,32 @@
import (
"context"
"encoding/json"
+ "fmt"
"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")
+ if folderName == "" {
+ return nil, fmt.Errorf("argument 'folder_name' is required")
+ }
+
fullPath, err := config.ResolveConfPath(basePath, folderName)
if err != nil {
return nil, err
@@ -39,5 +45,5 @@
}
jsonResult, _ := json.Marshal(result)
- return mcp.NewToolResultText(string(jsonResult)), nil
+ return mcpgo.NewToolResultText(string(jsonResult)), nil
}
diff --git a/mcp/config/config_modify.go b/mcp/config/config_modify.go
--- a/mcp/config/config_modify.go
+++ b/mcp/config/config_modify.go
@@ -4,13 +4,15 @@
"context"
"encoding/json"
"errors"
+ "fmt"
"path/filepath"
"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 +21,34 @@
// 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")
+ if relativePath == "" {
+ return nil, fmt.Errorf("argument 'relative_path' is required")
+ }
+ if _, exists := args["content"]; !exists || args["content"] == nil {
+ return nil, fmt.Errorf("argument 'content' is required")
+ }
+
// 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 +99,5 @@
}
jsonResult, _ := json.Marshal(result)
- return mcp.NewToolResultText(string(jsonResult)), nil
+ return mcpgo.NewToolResultText(string(jsonResult)), nil
}
diff --git a/mcp/config/config_rename.go b/mcp/config/config_rename.go
--- a/mcp/config/config_rename.go
+++ b/mcp/config/config_rename.go
@@ -3,42 +3,49 @@
import (
"context"
"encoding/json"
+ "fmt"
"os"
"path/filepath"
"strings"
"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")
+ if origName == "" {
+ return nil, fmt.Errorf("argument 'orig_name' is required")
+ }
+ if newName == "" {
+ return nil, fmt.Errorf("argument 'new_name' is required")
+ }
+
// 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 +54,7 @@
"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 +125,5 @@
}
jsonResult, _ := json.Marshal(result)
- return mcp.NewToolResultText(string(jsonResult)), nil
+ return mcpgo.NewToolResultText(string(jsonResult)), nil
}You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit b787572. Configure here.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Bug Fix
This PR fixes the panic that occurs when MCP config tools are called via Claude Desktop with protocol version
2025-11-25.Problem
All MCP configuration tools panic with:
This happens because the tool handlers used direct type assertions like
args["relative_path"].(string)which panic when the argument value isnil(not provided).Solution
Created helper functions in
internal/mcp/args.gofor safe argument extraction:GetString()- safely extracts string values, returns empty string if nilGetBool()- safely extracts boolean values, returns false if nilGetSlice()- safely extracts slice values, returns nil if nilUpdated all affected config handlers to use these safe extraction functions
Affected Tools
nginx_config_listnginx_config_getnginx_config_addnginx_config_modifynginx_config_renamenginx_config_mkdirnginx_config_historynginx_config_enableTesting
go buildgofmtandgoimportsgo vet