Semantic Pattern Adoption
This case study shows how gopls-mcp helps code AI agent to understand existing project patterns, and adopt the same semantic logic.
Background
Section titled “Background”In gopls-mcp implementation, we follow the same idea of go_diagnostics to implement go_build_check mcp tool.
We can ask claude to build check the project to ensure everything works.
Given a project where main_test.go exists (causing duplicate analysis) and main.go has an unused variable:
package main
func main() { x := 42 // unused}
// project structures:6 collapsed lines
// tree .// .// ├── CLAUDE.md// ├── go.mod// ├── main.go// └── main_test.gogopls-mcp@b6de828f1 reports duplicated diagnostics for project above.
❯ check whether current project could be built?
● I'll check if the current project builds successfully using the semantic build check tool. ⎿ { "diagnostics": [ { "code_snippet": "x := 42 // unused", "column": 2, "file": "/home/xieyuschen/codespace/go-playground/main.go", "line": 4, "message": "declared and not used: x", "severity": "Error" },7 collapsed lines
{ "code_snippet": "x := 42 // unused", "column": 2, "file": "/home/xieyuschen/codespace/go-playground/main.go", "line": 4, "message": "declared and not used: x", "severity": "Error" } ], ... }This indicates that we missed some logic handled inside gopls.
How gopls-mcp helps to solve this issue?
Section titled “How gopls-mcp helps to solve this issue?”Enter claude, ensures gopls-mcp is available as claude mcp setting is different per project.
❯ You should use precise semantic analysis gopls-mcp server to understand current go project, see @CLAUDE.md for more info. When i use gopls-mcp mcp tool go_build_check defined in @gopls/mcpbridge/core/server.go, I got the dulicapted diagnostics.
Native gopls diagnostics should handle this well, tell me how gopls does it and use the same way to fix it. By using gopls-mcp tools, AI agent can focus on reasoning further code changes instead of excessively searching symbols and usages from huge amount of code lines.
We can highlight some notable changes for AI agent when it integrates with gopls-mcp.
Understand Implementation
Section titled “Understand Implementation”The AI agent starts by searching for handleGoDiagnostics to understand how diagnostics are currently processed.
● I'll help you fix the duplicate diagnostics issue in gopls-mcp. Let me first search for the handleGoDiagnostics function to understand the current implementation.
● gopls-mcp - go_search (MCP)(Cwd: "/home/xieyuschen/codespace/gopls-mcp-case-st udy-mimic/gopls", query: "handleGoDiagnostics")4 collapsed lines
⎿ { "summary": "Found 2 symbol(s):\n - handleGoDiagnostics (Function in /home/xieyuschen/codespace/gopls-mcp-case-study-mimic/gopls/mcpbridge/c … +24 lines (ctrl+o to expand)
● Read(mcpbridge/core/gopls_wrappers.go) ⎿ Read 1550 linesTrace Native Behavior
Section titled “Trace Native Behavior”To adapt gopls’s behavior, the agent investigates PackageDiagnostics to see how the native implementation returns diagnostics.
● Now let me look at how gopls handles diagnostics deduplication. I'll search for the PackageDiagnostics function to understand how it returns diagnostics.
● gopls-mcp - go_search (MCP)(Cwd: "/home/xieyuschen/codespace/gopls-mcp-case-st udy-mimic/gopls", query: "PackageDiagnostics")5 collapsed lines
⎿ { "summary": "Found 8 symbol(s):\n - Snapshot.PackageDiagnostics (Meth od in /home/xieyuschen/codespace/gopls-mcp-case-study-mimic/gopls/inter … +92 lines (ctrl+o to expand)
● gopls-mcp - go_definition (MCP)(include_body: true, locator: {"symbol_name":"P ackageDiagnostics","context_file":"/home/xieyus chen/codespace/gopls-mcp-case-study-mimic/gopls /internal/cache/snapshot.go","kind":"method","l ine_hint":534,"parent_scope":"Snapshot"})4 collapsed lines
⎿ { "summary": "Definition found at /home/xieyuschen/codespace/gopls-mcp- case-study-mimic/gopls/internal/cache/snapshot.go:534\n\n**Name**: `Pac … +58 lines (ctrl+o to expand)Analyze Diagnostics Flow
Section titled “Analyze Diagnostics Flow”The agent continues by tracing workspaceDiagnosticsHandler and summarizeDiagnostics to see if deduplication happens at the workspace level.
● Now let me search for how gopls's native workspace diagnostics handler deduplicates diagnostics:
● gopls-mcp - go_search (MCP)(Cwd: "/home/xieyuschen/codespace/gopls-mcp-case-st udy-mimic/gopls", query:5 collapsed lines
"workspaceDiagnosticsHandler") ⎿ { "summary": "Found 1 symbol(s):\n - handler.workspaceDiagnosticsHandl er (Method in /home/xieyuschen/codespace/gopls-mcp-case-study-mimic/gop … +13 lines (ctrl+o to expand)
● Read(internal/mcp/workspace_diagnostics.go) ⎿ Read 93 lines
● Let me check the summarizeDiagnostics function to see if there's deduplication happening there:
● gopls-mcp - go_search (MCP)(Cwd: "/home/xieyuschen/codespace/gopls-mcp-case-st5 collapsed lines
udy-mimic/gopls", query: "summarizeDiagnostics") ⎿ { "summary": "Found 1 symbol(s):\n - summarizeDiagnostics (Function in /home/xieyuschen/codespace/gopls-mcp-case-study-mimic/gopls/internal/m … +13 lines (ctrl+o to expand)
● Read(internal/mcp/file_diagnostics.go) ⎿ Read 204 linesDiscover Deduplication Logic
Section titled “Discover Deduplication Logic”The agent correctly identifies CombineDiagnostics as the place where gopls handles deduplication.
● Let me look at the CombineDiagnostics function which might handle deduplication:
● gopls-mcp - go_search (MCP)(Cwd: "/home/xieyuschen/codespace/gopls-mcp-case-st udy-mimic/gopls", query: "CombineDiagnostics")5 collapsed lines
⎿ { "summary": "Found 1 symbol(s):\n - CombineDiagnostics (Function in / home/xieyuschen/codespace/gopls-mcp-case-study-mimic/gopls/internal/gol … +13 lines (ctrl+o to expand)
● gopls-mcp - go_definition (MCP)(include_body: true, locator: {"context_file":"/home/xieyuschen/codespace/gop ls-mcp-case-study-mimic/gopls/internal/golang/d iagnostics.go","kind":"function","line_hint":83 ,"symbol_name":"CombineDiagnostics"}) ⎿ { 3 collapsed lines
"summary": "Definition found at /home/xieyuschen/codespace/gopls-mcp- case-study-mimic/gopls/internal/golang/diagnostics.go:83\n\n**Name**: ` … +82 lines (ctrl+o to expand)Solution Pattern
Section titled “Solution Pattern”The agent then explains the root cause and the solution derived from the codebase analysis:
❯ how does gopls solve this issue? ⎿ ⧉ Selected 1 lines from mcpbridge/core/server.go in Visual Studio Code
● Based on my investigation, here\s how gopls solves the duplicate diagnostics issue:
The Root Cause
When a file belongs to multiple packages (e.g., a package with test files), PackageDiagnostics returns the same diagnostic multiple times - once for each package that includes the file.
gopls\s Solution: CombineDiagnostics
In gopls/internal/golang/diagnostics.go:83, gopls uses a map-based deduplication approach:
func CombineDiagnostics(tdiags []*cache.Diagnostic, adiags []*cache.Diagnostic) []*cache.Diagnostic { // Build index using a composite key type key struct { Range protocol.Range message string } ... }Implement Fix
Section titled “Implement Fix”Finally, the agent applies the findings by attempting to update the code to use a map-based deduplication strategy, mirroring the CombineDiagnostics implementation found in gopls/internal/golang/diagnostics.go:83.
● Now I understand the issue and the fix! Let me create a todo list for this fix:
● Now I\ll fix the handleGoDiagnostics function to deduplicate diagnostics using the same approach as gopls:
● Update(mcpbridge/core/gopls_wrappers.go) ⎿ User rejected update to mcpbridge/core/gopls_wrappers.go 389 return nil, nil, fmt.Errorf("diagnostics failed: %v", err) 390 } 391 392 - var diagnostics []api.Diagnostic 393 - var summary strings.Builder 394 - total := 0 392 + // Deduplicate diagnostics using the same approach as gopls 393 + // Use a map with key based on range and message to +identify duplicates 394 + type key struct { 395 + Range protocol.Range 396 + message string 397 + } 398 + uniqueDiags := make(map[key]*cache.Diagnostic) 399 + 400 for _, diags := range reports { 401 for _, diag := range diags { 402 - total++ 402 + k := key{diag.Range, diag.Message} 403 + if _, exists := uniqueDiags[k]; !exists { 404 + uniqueDiags[k] = diag 405 + } 406 + } 407 + } 408 + 409 + var diagnostics []api.Diagnostic 410 + var summary strings.Builder 411 + total := len(uniqueDiags) 412 + for _, diag := range uniqueDiags { 413 // Convert DiagnosticSeverity to string 414 severityStr := "Unknown" 415 switch diag.Severity {