Skip to content

feat: add workspace_symbol, implementation, and call_hierarchy tools#133

Open
scgm11 wants to merge 3 commits into
isaacphi:mainfrom
scgm11:feat/workspace-symbol-implementation-call-hierarchy
Open

feat: add workspace_symbol, implementation, and call_hierarchy tools#133
scgm11 wants to merge 3 commits into
isaacphi:mainfrom
scgm11:feat/workspace-symbol-implementation-call-hierarchy

Conversation

@scgm11
Copy link
Copy Markdown

@scgm11 scgm11 commented May 25, 2026

What

Surfaces three LSP methods this bridge already wraps at the client layer but doesn't expose as MCP tools.

  • workspace_symbol(query) — fuzzy symbol search across the workspace via workspace/symbol. Use BEFORE definition / references when you don't know which file a symbol lives in: one call replaces a grep -r "func MyFunc\|type MyFunc\|var MyFunc" scan. gopls / tsserver / clangd all support fuzzy matching by default.
  • implementation(filePath, line, column) — find every type or method that satisfies the interface at the given position via textDocument/implementation. Critical for Go and TypeScript: structural typing has no implements keyword for grep to match, so this is the only AST-aware path to enumerate an interface's satisfaction set.
  • call_hierarchy(filePath, line, column) — list direct callers AND callees of a function via the 3-call LSP dance (prepareCallHierarchyincomingCalls + outgoingCalls). Output mirrors gopls call_hierarchy: caller[N]: …, identifier: …, callee[N]: … markers.

Why

These three are the most-asked questions an editor's "Go to symbol", "Go to implementation", and "Show call hierarchy" buttons answer — and the LSP methods to power them are already in internal/lsp/methods.go (Symbol, Implementation, PrepareCallHierarchy, IncomingCalls, OutgoingCalls). Wrapping them as MCP tools means an agent driving this bridge gets the same surface a human gets in vscode/zed.

I'd been maintaining an out-of-tree fork to add these for our project's Claude Code wiring; opening this PR so it can live upstream instead.

How

  • Three new internal/tools/*.go files — thin wrappers around the existing client methods, following the exact shape of hover.go (URI scheme, 1→0 indexed conversion, client.OpenFile before position-based calls, error formatting).
  • Three new mcp.NewTool(...) registrations in tools.go, mirroring the hover / rename_symbol registration pattern (string + numeric args with float64/int type switching).
  • Small interface change in internal/protocol/interfaces.go: WorkspaceSymbolResult gains GetKind() and GetContainerName() methods. Both concrete types (WorkspaceSymbol and SymbolInformation) already have the underlying fields, so the implementation is two lines each. The new methods let workspace_symbol render results as <file>:<line>:<column> <name> <Kind>[ (in <ContainerName>)] — the canonical editor-symbol-picker format.

Verified

  • go build ./... clean.
  • go vet ./... clean.
  • go test ./internal/... — every existing test still green.
  • Built binary + MCP handshake against gopls (workspace = a 7923-file Go monorepo): tools/list returns 9 tools (the original 6 + the 3 new ones), each with the documented input schema. Sample workspace_symbol "ExpectedSchemaVersion" returns 4 matches (the constant + 3 tests).
  • Output formats match the gopls CLI's workspace_symbol / implementation / call_hierarchy subcommand output, so callers that already parse gopls CLI output don't need to rewrite their parser.

Compatibility

  • Interface change to WorkspaceSymbolResult is internal — only WorkspaceSymbol and SymbolInformation implement it, both controlled by this repo. No breaking changes to public consumers.
  • All new tools follow the documented MCP schema patterns (mcp.WithDescription / mcp.WithString / mcp.WithNumber / mcp.Required).
  • LocationLink fallback path in implementation.go handles servers that advertise linkSupport=true (rust-analyzer, clangd).

🤖 Generated with Claude Code

scgm11 added 3 commits May 25, 2026 09:42
The bridge already wraps the corresponding LSP methods at the
client layer (client.Symbol, client.Implementation,
client.PrepareCallHierarchy, client.IncomingCalls,
client.OutgoingCalls) — they were just never surfaced as MCP
tools. This PR adds three thin wrappers in internal/tools/ and
registers them in tools.go.

The new tools fill three navigation gaps the existing 6 don't
cover:

1. workspace_symbol(query) — fuzzy symbol search across the
   workspace. The existing `definition` and `references` tools
   require an EXACT symbol name (e.g. `mypackage.MyFunction`);
   workspace_symbol forwards a query string to LSP
   `workspace/symbol` for fuzzy match (gopls / tsserver / clangd
   all support fuzzy by default). One call replaces a
   `grep -r "func MyFunc\|type MyFunc\|var MyFunc"` scan.

2. implementation(filePath, line, column) — find every type or
   method that implements the interface at the given position.
   Wraps `textDocument/implementation`. Critical for Go and
   TypeScript: structural typing has no `implements` keyword for
   grep to match, so this is the only AST-aware path to
   enumerate an interface's satisfaction set.

3. call_hierarchy(filePath, line, column) — list direct callers
   AND callees of a function. Wraps the 3-call LSP dance:
   prepareCallHierarchy → incomingCalls + outgoingCalls. Output
   mirrors `gopls call_hierarchy`: `caller[N]: …` lines per
   incoming call, an `identifier: …` line for the target,
   `callee[N]: …` lines per outgoing call.

Interface change: WorkspaceSymbolResult gains GetKind() and
GetContainerName() methods (both concrete types — WorkspaceSymbol
and SymbolInformation — already have the underlying fields, so
the implementation is trivial). This lets workspace_symbol render
results as `<file>:<line>:<column> <name> <Kind>[ (in
<ContainerName>)]`, matching what every editor's symbol picker
shows.

URI handling, position 0/1-index conversion, file open semantics
(client.OpenFile before position-based calls), and error
formatting all follow the patterns established by the existing
hover / definition / references tools.

Verified:
- `go build ./...` clean.
- `go vet ./...` clean.
- `go test ./internal/...` — every existing test still green.
- Built binary + MCP handshake against gopls (workspace =
  large Go monorepo, 7923 files): `tools/list` returns 9 tools
  (the original 6 + the 3 new ones), each with the documented
  input schema.
…erarchy

Adds Go integration tests for the three new tools, mirroring the
shape of integrationtests/tests/go/hover/hover_test.go (table-
driven cases + TestSuite + SnapshotTest + per-case assertions on
expected substrings).

Coverage per tool:

  workspace_symbol/
    ExactType            — query "SharedStruct"
    ExactInterface       — query "SharedInterface"
    ExactConstant        — query "SharedConstant"
    WorkspaceUniqueType  — query "TestStruct"  (workspace-unique
                            so the snapshot stays stable across Go
                            stdlib versions; a bare "Method" query
                            would match every Method in reflect /
                            abi / runtime and the snapshot would
                            flap on every Go release)
    NoMatch              — query that doesn't exist
    EmptyQuery           — validation gate fires before the LSP
                            call (test in a separate function so
                            the snapshot tests don't have to deal
                            with an error case)

  implementation/
    InterfaceToImpl        — cursor on `SharedInterface`
    InterfaceMethodToImpl  — cursor on a method declared in the
                              interface body
    NoImpls                — cursor on a const; documents that
                              gopls returns an LSP error
                              ("is a const, not a type") while
                              spec-compliant servers return an
                              empty list — accepts both shapes

  call_hierarchy/
    OnMethodName       — cursor on SharedStruct.Method
    OnInterfaceImpl    — cursor on SharedStruct.Process (a method
                          satisfying SharedInterface)
    NonFunction        — cursor on a const; same dual-shape
                          assertion as implementation/NoImpls
                          (gopls error vs spec-compliant empty)

All 14 test cases pass on the verification run (no UPDATE_SNAPSHOTS
flag) — snapshots are deterministic.

Two LSP-behavior notes baked into the dual-shape assertions
(NoImpls / NonFunction):
- LSP spec says prepareCallHierarchy / textDocument/implementation
  SHOULD return null / empty list for invalid positions.
- gopls instead returns a JSON-RPC error with a clear message
  ("X is a const, not a type" / "X is not a function").

Both are acceptable from the agent's perspective — the test
accepts either. A future tsserver-targeted version of these tests
would likely exercise the spec-compliant path.
…nce bug

Three issues found while reviewing the new integration tests for
machine-specific leaks. Snapshots generated on one developer's
machine should be byte-equal to the same snapshot generated on a
CI runner — otherwise every PR that touches a snapshotted tool
churns a diff for every contributor.

1. `normalizePaths` (in integrationtests/tests/common/helpers.go)
   handled only the FIRST `/workspace/` occurrence per line via
   `strings.Split(..., "/workspace/")` + reading only `parts[1]`.
   call_hierarchy output embeds the workspace path TWICE per
   `caller[N]:` row (once for the range location, once for the
   symbol declaration), so the trailing path was silently dropped
   AND the absolute prefix between them leaked through verbatim
   (e.g. `/private/var/folders/.../TestCallHierarchy_*`).

   Rewrote as a forward-scanning loop in `normalizeWorkspaceMarker`
   that walks each marker occurrence and replaces the preceding
   path-segment prefix. Idempotent (advances past the freshly-
   written placeholder so the trailing `/workspace/` doesn't re-
   match).

2. workspace_symbol's `TestStruct` query was fuzzy-matched by
   gopls against Go runtime symbols (`stringStruct`, `stringStructOf`,
   `stringStructDWARF`), pulling Go-stdlib paths
   (`<homebrew>/Cellar/go/<version>/libexec/src/runtime/...` on
   macOS, `/usr/local/go/src/runtime/...` on Linux) into the
   snapshot. Switched to `AnotherConsumer` — present only in the
   test workspace fixture, no substring overlap with any stdlib
   symbol name.

3. call_hierarchy's `OnInterfaceImpl` case targeted `Process` (a
   method that calls `fmt.Printf`), so gopls' callee[0] resolved
   into the Go stdlib's `fmt/print.go` with the same path-leak
   problem. Switched to `GetName` — also satisfies SharedInterface
   but has no body calls.

Verified: all 14 cases pass on a cold run without UPDATE_SNAPSHOTS.
Final snapshot scan finds zero machine-specific paths.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant