From 2b2447ffc93b335aa57491cefe7cedd7e9fac2b5 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 13 Jun 2025 15:31:32 +0000 Subject: [PATCH 1/2] Fix MCPConfig.UnmarshalJSON to use JSONC parsing for comment support This completes the JSONC parsing fix started in PR #386. While the loadConfig function was updated to use jsonc.Unmarshal, the custom MCPConfig.UnmarshalJSON method still used json.Unmarshal in three locations, causing the 'invalid character \'/\' looking for beginning of value' error to persist when parsing VS Code settings with comments. Updated all three json.Unmarshal calls to jsonc.Unmarshal: - Line 69: Unmarshaling into auxiliary struct - Line 74: Unmarshaling into map for extra fields - Line 85: Unmarshaling individual values in extra fields This ensures consistent JSONC parsing throughout the MCP config system. Co-Authored-By: Rick Blalock --- internal/mcp/config.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/mcp/config.go b/internal/mcp/config.go index f2926355..0d35c0d4 100644 --- a/internal/mcp/config.go +++ b/internal/mcp/config.go @@ -66,12 +66,12 @@ func (c *MCPConfig) UnmarshalJSON(data []byte) error { }{ Alias: (*Alias)(c), } - if err := json.Unmarshal(data, &aux); err != nil { + if err := jsonc.Unmarshal(data, &aux); err != nil { return err } // Unmarshal into a map to find extra fields var all map[string]json.RawMessage - if err := json.Unmarshal(data, &all); err != nil { + if err := jsonc.Unmarshal(data, &all); err != nil { return err } // Remove known fields @@ -82,7 +82,7 @@ func (c *MCPConfig) UnmarshalJSON(data []byte) error { c.Extra = make(map[string]interface{}) for k, v := range all { var val interface{} - if err := json.Unmarshal(v, &val); err != nil { + if err := jsonc.Unmarshal(v, &val); err != nil { c.Extra[k] = string(v) // fallback to raw string } else { c.Extra[k] = val From 0782ee849d535b980c281e857dbf78efc1e27ae6 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 13 Jun 2025 16:02:50 +0000 Subject: [PATCH 2/2] Complete JSONC parsing fix: convert MarshalJSON method and add comprehensive tests - Fix remaining json.Unmarshal call in MarshalJSON method to use jsonc.Unmarshal - Add comprehensive test cases for JSONC comment support including: - User's exact case: /* test */ comment at top of JSON - Single line comments (//) - Multi-line comments (/* */) - Mixed comment types - Empty JSON with comments - Comments at end of JSON - Complete load-save cycle testing - Cursor settings with comments testing This completes the JSONC parsing implementation throughout the entire MCP config system, resolving the persistent 'unexpected end of JSON input' error when parsing VS Code/Cursor settings files containing comments. Co-Authored-By: Rick Blalock --- internal/mcp/config.go | 2 +- internal/mcp/config_jsonc_test.go | 252 ++++++++++++++++++++++++++++++ 2 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 internal/mcp/config_jsonc_test.go diff --git a/internal/mcp/config.go b/internal/mcp/config.go index 0d35c0d4..a7a00ccb 100644 --- a/internal/mcp/config.go +++ b/internal/mcp/config.go @@ -105,7 +105,7 @@ func (c *MCPConfig) MarshalJSON() ([]byte, error) { } // Unmarshal back into a map to merge with Extra var m map[string]interface{} - if err := json.Unmarshal(data, &m); err != nil { + if err := jsonc.Unmarshal(data, &m); err != nil { return nil, err } for k, v := range c.Extra { diff --git a/internal/mcp/config_jsonc_test.go b/internal/mcp/config_jsonc_test.go new file mode 100644 index 00000000..4b37bb61 --- /dev/null +++ b/internal/mcp/config_jsonc_test.go @@ -0,0 +1,252 @@ +package mcp + +import ( + "os" + "path/filepath" + "testing" +) + +func TestJSONCCommentSupport(t *testing.T) { + tests := []struct { + name string + jsonContent string + expectError bool + }{ + { + name: "User's exact case: /* test */ at top", + jsonContent: `/* test */ +{ + "editor.fontSize": 14, + "workbench.colorTheme": "Default Dark+", + "mcpServers": { + "agentuity": { + "command": "npx", + "args": ["-y", "@agentuity/mcp-server"], + "env": { + "AGENTUITY_API_KEY": "${AGENTUITY_API_KEY}" + } + } + } +}`, + expectError: false, + }, + { + name: "Single line comments", + jsonContent: `{ + "editor.fontSize": 14, + "mcpServers": { + "agentuity": { + "command": "npx", // inline comment + "args": ["-y", "@agentuity/mcp-server"] + } + } +}`, + expectError: false, + }, + { + name: "Multi-line comments", + jsonContent: `{ + /* This is a + multi-line comment */ + "mcpServers": { + "test": { + "command": "test" + } + }, + /* Another comment */ + "ampMcpServers": { + "amp": { + "command": "amp" + } + } +}`, + expectError: false, + }, + { + name: "Mixed comments comprehensive", + jsonContent: `{ + /* test comment at start */ + "editor.fontSize": 14, + "workbench.colorTheme": "Default Dark+", + "mcpServers": { + "agentuity": { + "command": "npx", // inline comment + "args": ["-y", "@agentuity/mcp-server"], + "env": { + "AGENTUITY_API_KEY": "${AGENTUITY_API_KEY}" /* env var comment */ + } + } + }, + /* Extension settings */ + "extensions.autoUpdate": false, + "ampMcpServers": { + "test": { + "command": "test" + } + } +}`, + expectError: false, + }, + { + name: "Empty JSON with comment", + jsonContent: `/* test */ +{ +}`, + expectError: false, + }, + { + name: "Comment at end", + jsonContent: `{ + "mcpServers": { + "test": { + "command": "test" + } + } +} +/* end comment */`, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir := t.TempDir() + configPath := filepath.Join(tmpDir, "test_config.json") + + err := os.WriteFile(configPath, []byte(tt.jsonContent), 0644) + if err != nil { + t.Fatalf("Failed to write test file: %v", err) + } + + config, err := loadConfig(configPath) + if tt.expectError { + if err == nil { + t.Errorf("Expected error but got none") + } + return + } + + if err != nil { + t.Errorf("Unexpected error: %v", err) + return + } + + if config == nil { + t.Errorf("Config should not be nil") + return + } + + t.Logf("Successfully parsed config with %d mcpServers, %d ampMcpServers, %d extra fields", + len(config.MCPServers), len(config.AMPMCPServers), len(config.Extra)) + }) + } +} + +func TestCompleteLoadSaveCycleWithComments(t *testing.T) { + jsonWithComments := `/* test comment */ +{ + "editor.fontSize": 14, + "workbench.colorTheme": "Default Dark+", + "mcpServers": { + "agentuity": { + "command": "npx", + "args": ["-y", "@agentuity/mcp-server"], + "env": { + "AGENTUITY_API_KEY": "${AGENTUITY_API_KEY}" + } + } + }, + /* Extension settings */ + "extensions.autoUpdate": false +}` + + tmpDir := t.TempDir() + configPath := filepath.Join(tmpDir, "test_config.json") + + err := os.WriteFile(configPath, []byte(jsonWithComments), 0644) + if err != nil { + t.Fatalf("Failed to write test file: %v", err) + } + + config, err := loadConfig(configPath) + if err != nil { + t.Fatalf("Failed to load config: %v", err) + } + + if len(config.MCPServers) != 1 { + t.Errorf("Expected 1 mcpServer, got %d", len(config.MCPServers)) + } + + if len(config.Extra) != 3 { + t.Errorf("Expected 3 extra fields, got %d", len(config.Extra)) + } + + marshaledData, err := config.MarshalJSON() + if err != nil { + t.Fatalf("Failed to marshal config: %v", err) + } + + if len(marshaledData) == 0 { + t.Errorf("Marshaled data should not be empty") + } + + t.Logf("Successfully completed load-save cycle with comments") +} + +func TestCursorSettingsWithComments(t *testing.T) { + cursorSettings := `/* test */ +{ + "editor.fontSize": 14, + "workbench.colorTheme": "Default Dark+", + "mcpServers": { + "agentuity": { + "command": "npx", + "args": ["-y", "@agentuity/mcp-server"], + "env": { + "AGENTUITY_API_KEY": "${AGENTUITY_API_KEY}" + } + }, + "filesystem": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/allowed/files"] + } + }, + "extensions.autoUpdate": false +}` + + tmpDir := t.TempDir() + settingsPath := filepath.Join(tmpDir, "settings.json") + + err := os.WriteFile(settingsPath, []byte(cursorSettings), 0644) + if err != nil { + t.Fatalf("Failed to write Cursor settings file: %v", err) + } + + config, err := loadConfig(settingsPath) + if err != nil { + t.Fatalf("Failed to load Cursor settings with comments: %v", err) + } + + if len(config.MCPServers) != 2 { + t.Errorf("Expected 2 mcpServers in Cursor settings, got %d", len(config.MCPServers)) + } + + if _, exists := config.MCPServers["agentuity"]; !exists { + t.Errorf("Expected 'agentuity' mcpServer in Cursor settings") + } + + if _, exists := config.MCPServers["filesystem"]; !exists { + t.Errorf("Expected 'filesystem' mcpServer in Cursor settings") + } + + marshaledData, err := config.MarshalJSON() + if err != nil { + t.Fatalf("Failed to marshal Cursor settings: %v", err) + } + + if len(marshaledData) == 0 { + t.Errorf("Marshaled Cursor settings should not be empty") + } + + t.Logf("Successfully processed Cursor settings with /* test */ comment") +}