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

@scgm11 scgm11 commented May 25, 2026

Copy link
Copy Markdown

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.
judggernaut added a commit to judggernaut/mcp-language-server that referenced this pull request Jul 3, 2026
…rrences

Discovered while porting upstream PR isaacphi#133's call_hierarchy tool, whose
output embeds the same workspace path twice per line (once per LSP
location field). The old normalizePaths used strings.Split(line,
"/workspace/") and kept only parts[1], which had two bugs:

  1. It replaced the ENTIRE line with just the placeholder + remainder,
     silently discarding any text before the first '/workspace/' match.
     Every 'No diagnostics found for <path>' and every 'File: <path>'
     line in every snapshot across all five languages had that leading
     text silently dropped, undetected because the test's actual
     strings.Contains() assertions check the raw (non-normalized)
     result, not the snapshot, so no test was ever wrong, only the
     auxiliary snapshot file was incomplete.
  2. Only the first '/workspace/' occurrence on a line was normalized;
     a second occurrence kept the real machine-local absolute path,
     which would make call_hierarchy snapshots (two paths per caller
     line) non-portable across machines/CI runners.

Replace it with normalizeWorkspaceMarker, which scans a line for every
occurrence of the marker, walks backward to the nearest path-segment
boundary to find where the absolute path starts, and rewrites just
that segment, preserving everything else on the line. Regenerated
every existing snapshot with UPDATE_SNAPSHOTS=true and confirmed each
diff is exactly the previously-dropped prefix text reappearing (e.g.
'File: ' before a path, 'No diagnostics found for ' before a path) with
no other change; all five language integration suites still pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
judggernaut added a commit to judggernaut/mcp-language-server that referenced this pull request Jul 3, 2026
Upstream PR isaacphi#133 (isaacphi/mcp-language-server).

The existing definition/references tools require an exact symbol
name and answer "where is X defined" / "where is X used". Three
structural questions had no tool at all, despite the LSP client
already having the underlying methods (Symbol, Implementation,
PrepareCallHierarchy, IncomingCalls, OutgoingCalls) wired up and
unused:

  - workspace_symbol: search the whole workspace by (possibly fuzzy)
    query when you don't know which file a symbol lives in, replacing
    an N-call grep-the-codebase scan with one workspace/symbol call.
  - implementation: find every concrete type/method that satisfies an
    interface. Go and TypeScript both use structural typing with no
    "implements" keyword for grep to match, so this is the only
    reliable way to enumerate an interface's satisfaction set.
  - call_hierarchy: list direct callers AND callees of a function or
    method in one call (textDocument/prepareCallHierarchy +
    callHierarchy incoming/outgoing calls), for refactor impact
    analysis and data-flow tracing.

internal/protocol/interfaces.go: extend WorkspaceSymbolResult with
GetKind()/GetContainerName() (both WorkspaceSymbol and
SymbolInformation already carry this data via BaseSymbolInformation-
shaped fields) so workspace_symbol output can show a symbol's kind and
containing package/class.

Adapted from the upstream PR to this fork's conventions: uses
protocol.URIFromPath / DocumentUri.Path() instead of raw file:// string
concatenation (consistent with the Windows URI fix), and adds the same
ReadOnlyHint tool annotations the other read-only tools in this fork
already carry (see the earlier MCP tool annotations commit).

Ported the three Go integration test suites unchanged (line/column
positions and symbol names in the shared test fixtures matched this
fork's copy exactly) and generated fresh snapshots, which came out
byte-identical to the upstream PR's own committed snapshots.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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