feat(lsp): query by symbol_name; document ast_query match limits

Two ast_query/lsp_query ergonomics improvements that share the tool registration and help files:

- lsp_query accepts symbol_name (+symbol_kind) and resolves the position via AST (resolveLSPSymbolPosition), so callers need not know line:col; line/column are no longer required. SymbolCandidate gains NameLine/NameColumn for the precise identifier position. Ambiguous/unknown names error with candidates / a 'did you mean' hint. Adds TestResolveLSPSymbolPosition.
- ast_query help + tool description now document that matching is keyword/kind-based, not structural: pattern arg/return constraints are not enforced; use name_exact/name_matches/kind_in to filter. Fixes a misleading 'functions returning error' example.
This commit is contained in:
2026-06-20 00:33:32 +01:00
parent b6489ee4bc
commit cc1661d295
5 changed files with 149 additions and 23 deletions
+46 -10
View File
@@ -10,6 +10,7 @@ import (
"github.com/lukaszraczylo/mcp-filepuff/internal/lsp"
"github.com/lukaszraczylo/mcp-filepuff/internal/parser"
"github.com/lukaszraczylo/mcp-filepuff/pkg/errors"
"github.com/lukaszraczylo/mcp-filepuff/pkg/protocol"
"github.com/mark3labs/mcp-go/mcp"
)
@@ -26,20 +27,30 @@ func (s *Server) handleLSPQuery(ctx context.Context, request mcp.CallToolRequest
return mcp.NewToolResultError("file is required"), nil
}
line := request.GetInt("line", 0)
if line <= 0 {
return mcp.NewToolResultError("line must be positive"), nil
}
col := request.GetInt("column", 0)
if col <= 0 {
return mcp.NewToolResultError("column must be positive"), nil
}
if !s.cfg.IsPathAllowed(file) {
return mcp.NewToolResultError("file is outside workspace root"), nil
}
// Position can be given directly (line+column) or resolved from a symbol name.
var line, col int
if symbolName := request.GetString("symbol_name", ""); symbolName != "" {
symbolKind := protocol.SymbolKind(request.GetString("symbol_kind", ""))
resolvedLine, resolvedCol, errResult := s.resolveLSPSymbolPosition(ctx, file, symbolName, symbolKind)
if errResult != nil {
return errResult, nil
}
line, col = resolvedLine, resolvedCol
} else {
line = request.GetInt("line", 0)
if line <= 0 {
return mcp.NewToolResultError("line must be positive (or provide symbol_name)"), nil
}
col = request.GetInt("column", 0)
if col <= 0 {
return mcp.NewToolResultError("column must be positive (or provide symbol_name)"), nil
}
}
verbose := request.GetBool("verbose", false)
switch action {
@@ -76,6 +87,31 @@ func (s *Server) handleLSPQuery(ctx context.Context, request mcp.CallToolRequest
}
}
// resolveLSPSymbolPosition resolves a symbol name to the 1-indexed line:column of
// its name identifier, for use as an LSP position. Returns an error result when the
// file can't be read/parsed, the symbol is not found (with a "did you mean" hint),
// or the name is ambiguous (listing candidates).
func (s *Server) resolveLSPSymbolPosition(ctx context.Context, file, symbolName string, symbolKind protocol.SymbolKind) (line, col int, errResult *mcp.CallToolResult) {
content, err := os.ReadFile(file)
if err != nil {
return 0, 0, mcp.NewToolResultError(fmt.Sprintf("failed to read file: %s", errors.SanitizeError(err)))
}
result, err := s.parser.Parse(ctx, file, content)
if err != nil {
return 0, 0, mcp.NewToolResultError(fmt.Sprintf("failed to parse file: %s", errors.SanitizeError(err)))
}
candidates := parser.FindSymbolCandidates(result.Tree, content, file, symbolName, symbolKind)
switch len(candidates) {
case 0:
return 0, 0, mcp.NewToolResultError(fmt.Sprintf("symbol %q not found in %s%s", symbolName, file, s.nearestSymbolSuggestion(ctx, file, content, symbolName)))
case 1:
return candidates[0].NameLine, candidates[0].NameColumn, nil
default:
return 0, 0, mcp.NewToolResultError(fmt.Sprintf("symbol %q is ambiguous in %s (%d matches): %s — disambiguate with symbol_kind, or pass line+column",
symbolName, file, len(candidates), formatSymbolCandidates(candidates)))
}
}
// lspHover performs hover (symbol info) for the given position.
func (s *Server) lspHover(ctx context.Context, file string, line, col int, verbose bool) (*mcp.CallToolResult, error) {
hover, err := s.lspManager.Hover(ctx, file, line, col)