multi-fixes

This commit is contained in:
2026-02-22 15:24:48 +00:00
parent 982c2c8b44
commit e2dc4a1f40
11 changed files with 498 additions and 48 deletions
+33
View File
@@ -104,6 +104,39 @@ After downloading or building the binary, configure it in Claude Code:
See the [Claude Code MCP documentation](https://code.claude.com/docs/en/mcp) for more details. See the [Claude Code MCP documentation](https://code.claude.com/docs/en/mcp) for more details.
### Recommended Claude Code Configuration
#### Selective Tool Deferral
For optimal performance, keep the most frequently used tools loaded at startup and defer the rest. This follows [Anthropic's recommended practice](https://www.anthropic.com/engineering/advanced-tool-use) of loading 3-5 high-use tools immediately:
```json
{
"mcpServers": {
"filepuff": {
"command": "mcp-filepuff",
"args": ["-workspace", "."],
"alwaysAllow": ["file_read", "file_search", "edit_apply", "edit_preview"]
}
}
}
```
This keeps `file_read`, `file_search`, `edit_apply`, and `edit_preview` immediately available while deferring less frequently used tools (`ping`, `ast_query`, `symbol_at`, `find_definition`, `find_references`).
#### System Prompt Guidance
Add the following to your `CLAUDE.md` to help Claude understand the available tool categories:
```markdown
You have access to filepuff MCP tools providing:
- File reading with AST symbol summaries (file_read)
- Fast regex search powered by ripgrep (file_search)
- Structural code pattern matching across 9+ languages (ast_query)
- LSP-powered go-to-definition, find-references, and symbol info (find_definition, find_references, symbol_at)
- AST-aware file editing with syntax validation and preview (edit_preview, edit_apply)
```
## Usage ## Usage
### Running the Server (Standalone) ### Running the Server (Standalone)
+18 -5
View File
@@ -47,9 +47,10 @@ Health check - returns pong to verify the server is running
{"tool": "ping"} {"tool": "ping"}
``` ```
**Returns:** `"pong"` text string.
**Notes:** **Notes:**
- Returns: "pong"
- Use to verify server connectivity - Use to verify server connectivity
--- ---
@@ -84,6 +85,8 @@ Search for text patterns in files using ripgrep. Supports regex patterns, file t
{"pattern": "TODO", "ignore_case": true, "context_lines": 3} {"pattern": "TODO", "ignore_case": true, "context_lines": 3}
``` ```
**Returns:** Results grouped by file with match context. Format: `"Found N matches in M files:"` followed by file sections, each with matching lines prefixed by `"L{line}│"` and context lines prefixed by `" │"`.
**Notes:** **Notes:**
- Requires ripgrep (rg) to be installed - Requires ripgrep (rg) to be installed
@@ -128,6 +131,8 @@ Read a file's contents with optional line range and AST symbol summary
{"path": "large_file.go", "max_lines": 100} {"path": "large_file.go", "max_lines": 100}
``` ```
**Returns:** File content with numbered lines (format: `" 12│ line text"`). When `include_ast=true`: prepends symbol summary (`"**file.go** (N lines, go)\nSymbols:\n func Name L12\n struct Config L45"`). When `symbols_only=true`: returns only the symbol summary (~95% fewer tokens). When `max_lines` is set: truncates output with `"[... N more lines omitted]"` notice.
**Notes:** **Notes:**
- symbols_only mode reduces token usage by ~90-98% - symbols_only mode reduces token usage by ~90-98%
@@ -170,6 +175,8 @@ Search for AST patterns in code files. Use code patterns with $VAR placeholders
{"pattern": "function $NAME($PROPS) { $$$BODY }", "language": "javascript", "name_matches": "^[A-Z]"} {"pattern": "function $NAME($PROPS) { $$$BODY }", "language": "javascript", "name_matches": "^[A-Z]"}
``` ```
**Returns:** `"Found N match(es):"` followed by entries in format `"**file:line** (node_type)"` with code blocks and captured variables (`$NAME=value`). Returns `"No matches found."` when no results.
**Notes:** **Notes:**
- $NAME captures identifiers - $NAME captures identifiers
@@ -201,6 +208,8 @@ Get information about the symbol at a specific position in a file. Returns type,
{"file": "server.go", "line": 25, "column": 10} {"file": "server.go", "line": 25, "column": 10}
``` ```
**Returns:** `"**Symbol Information**"` followed by hover/type information from LSP, or `"**Symbol Information** (AST fallback)"` with node type and text when LSP unavailable. Returns `"No symbol information available at this position."` when nothing is found.
**Notes:** **Notes:**
- Requires LSP server for full type information - Requires LSP server for full type information
@@ -228,10 +237,11 @@ Find the definition of the symbol at a specific position. Uses LSP to locate whe
{"file": "server.go", "line": 42, "column": 15} {"file": "server.go", "line": 42, "column": 15}
``` ```
**Returns:** `"Found N definition(s):"` with entries showing `"**file:line:column**"` and a 3-line code preview with the target line marked by `">"`. Returns `"No definition found."` when the symbol has no definition.
**Notes:** **Notes:**
- Requires language server for the file type - Requires language server for the file type
- Returns file path, line, and column of definition
- Shows code preview at definition location - Shows code preview at definition location
--- ---
@@ -261,6 +271,8 @@ Find all references to the symbol at a specific position. Uses LSP to locate all
{"file": "server.go", "line": 42, "column": 15, "include_declaration": false} {"file": "server.go", "line": 42, "column": 15, "include_declaration": false}
``` ```
**Returns:** `"Found N reference(s):"` grouped by file, each showing `"**file** (count)"` with locations as `"L{line}:{column}"`. Returns `"No references found."` when no usages exist.
**Notes:** **Notes:**
- Requires language server for the file type - Requires language server for the file type
@@ -305,10 +317,10 @@ Preview an edit without applying it. Uses AST-aware editing for code files (Go,
{"file": "package.json", "operation": "replace", "selector_pattern": "\\"version\\":\\\\s*\\"[^\\"]+\\"", "new_content": "\\"version\\": \\"2.0.0\\""} {"file": "package.json", "operation": "replace", "selector_pattern": "\\"version\\":\\\\s*\\"[^\\"]+\\"", "new_content": "\\"version\\": \\"2.0.0\\""}
``` ```
**Returns:** `"**Edit Preview**"` followed by a unified diff showing proposed changes. Does not modify the file. For code files: uses AST-aware mode with syntax validation. For other files: uses text-based mode.
**Notes:** **Notes:**
- Returns a diff showing proposed changes
- Does not modify the file
- Use to validate changes before applying - Use to validate changes before applying
--- ---
@@ -344,9 +356,10 @@ Apply an edit to a file. Uses AST-aware editing for code files (Go, TypeScript,
{"file": "config.yaml", "operation": "replace", "selector_line": 5, "selector_line_end": 10, "new_content": "database:\\n host: production.db.example.com\\n port: 5432"} {"file": "config.yaml", "operation": "replace", "selector_line": 5, "selector_line_end": 10, "new_content": "database:\\n host: production.db.example.com\\n port: 5432"}
``` ```
**Returns:** `"**Edit Applied Successfully**"` followed by a unified diff of the changes made. For code files, validates syntax before writing — returns an error if the edit would produce invalid syntax.
**Notes:** **Notes:**
- For code files: validates syntax before and after edit
- Preserves file permissions - Preserves file permissions
- Uses atomic writes for safety - Uses atomic writes for safety
- File locking prevents concurrent edits - File locking prevents concurrent edits
+113 -22
View File
@@ -174,8 +174,6 @@ func (e *Engine) performASTEdit(ctx context.Context, edit *ASTEdit, apply bool)
result := &EditResult{ result := &EditResult{
Success: true, Success: true,
Diff: diff, Diff: diff,
OriginalContent: string(content),
NewContent: string(newContent),
Applied: false, Applied: false,
} }
@@ -233,8 +231,6 @@ func (e *Engine) performTextEdit(_ context.Context, edit *ASTEdit, apply bool) (
result := &EditResult{ result := &EditResult{
Success: true, Success: true,
Diff: diff, Diff: diff,
OriginalContent: string(content),
NewContent: string(newContent),
Applied: false, Applied: false,
} }
@@ -568,14 +564,22 @@ func indentContent(content string, indent string) string {
return strings.Join(lines, "\n") return strings.Join(lines, "\n")
} }
// diffLine represents a single line in the diff with its type and content.
type diffLine struct {
op diffmatchpatch.Operation
text string // line content without trailing newline
oldN int // 1-based line number in original (0 if insert)
newN int // 1-based line number in modified (0 if delete)
}
// generateDiff creates a unified diff between original and modified content. // generateDiff creates a unified diff between original and modified content.
// Uses line-level Myers diff algorithm for accurate and readable diffs. // Uses line-level Myers diff algorithm and outputs a proper unified diff
// with context lines (3 before/after each change, merging close hunks).
func (e *Engine) generateDiff(original, modified, filename string) string { func (e *Engine) generateDiff(original, modified, filename string) string {
dmp := e.dmp dmp := e.dmp
// Use line-level diffing: encode each line as a single character, // Use line-level diffing: encode each line as a single character,
// diff the encoded strings, then decode back to real lines. // diff the encoded strings, then decode back to real lines.
// This prevents character-level diffs from splitting lines incorrectly.
chars1, chars2, lineArray := dmp.DiffLinesToChars(original, modified) chars1, chars2, lineArray := dmp.DiffLinesToChars(original, modified)
diffs := dmp.DiffMain(chars1, chars2, false) diffs := dmp.DiffMain(chars1, chars2, false)
diffs = dmp.DiffCharsToLines(diffs, lineArray) diffs = dmp.DiffCharsToLines(diffs, lineArray)
@@ -583,30 +587,117 @@ func (e *Engine) generateDiff(original, modified, filename string) string {
// Cleanup for readability // Cleanup for readability
diffs = dmp.DiffCleanupSemantic(diffs) diffs = dmp.DiffCleanupSemantic(diffs)
// Convert to unified diff format // Flatten diffs into individual lines with line numbers
var lines []diffLine
oldLine := 1
newLine := 1
for _, d := range diffs {
rawLines := strings.SplitAfter(d.Text, "\n")
for _, raw := range rawLines {
if raw == "" {
continue
}
text := strings.TrimSuffix(raw, "\n")
switch d.Type {
case diffmatchpatch.DiffEqual:
lines = append(lines, diffLine{op: d.Type, text: text, oldN: oldLine, newN: newLine})
oldLine++
newLine++
case diffmatchpatch.DiffDelete:
lines = append(lines, diffLine{op: d.Type, text: text, oldN: oldLine})
oldLine++
case diffmatchpatch.DiffInsert:
lines = append(lines, diffLine{op: d.Type, text: text, newN: newLine})
newLine++
}
}
}
// Identify indices of changed lines
const contextSize = 3
var changedIndices []int
for i, l := range lines {
if l.op != diffmatchpatch.DiffEqual {
changedIndices = append(changedIndices, i)
}
}
if len(changedIndices) == 0 {
return "" // no changes
}
// Build inclusion ranges: for each changed line, include contextSize lines before/after.
// Merge overlapping or adjacent ranges (gap <= 2*contextSize = 6 context lines).
type indexRange struct{ start, end int } // inclusive
var ranges []indexRange
for _, ci := range changedIndices {
rStart := ci - contextSize
if rStart < 0 {
rStart = 0
}
rEnd := ci + contextSize
if rEnd >= len(lines) {
rEnd = len(lines) - 1
}
if len(ranges) > 0 && rStart <= ranges[len(ranges)-1].end+1 {
// Merge with previous range
ranges[len(ranges)-1].end = rEnd
} else {
ranges = append(ranges, indexRange{rStart, rEnd})
}
}
// Emit unified diff
var buf bytes.Buffer var buf bytes.Buffer
buf.WriteString(fmt.Sprintf("--- %s\n", filename)) buf.WriteString(fmt.Sprintf("--- %s\n", filename))
buf.WriteString(fmt.Sprintf("+++ %s\n", filename)) buf.WriteString(fmt.Sprintf("+++ %s\n", filename))
for _, diff := range diffs { for _, r := range ranges {
// SplitAfter preserves the trailing \n on each line, so we can // Determine hunk header line numbers
// distinguish real lines from a trailing empty split artifact. var oldStart, oldCount, newStart, newCount int
lines := strings.SplitAfter(diff.Text, "\n") for i := r.start; i <= r.end; i++ {
for _, line := range lines { l := lines[i]
if line == "" { switch l.op {
continue case diffmatchpatch.DiffEqual:
if oldCount == 0 {
oldStart = l.oldN
}
if newCount == 0 {
newStart = l.newN
}
oldCount++
newCount++
case diffmatchpatch.DiffDelete:
if oldCount == 0 {
oldStart = l.oldN
}
if newCount == 0 {
// Set newStart from context or next available
newStart = l.oldN // approximate
}
oldCount++
case diffmatchpatch.DiffInsert:
if newCount == 0 {
newStart = l.newN
}
if oldCount == 0 {
oldStart = l.newN // approximate
}
newCount++
}
} }
// Remove trailing newline for display — we add our own. buf.WriteString(fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", oldStart, oldCount, newStart, newCount))
cleanLine := strings.TrimSuffix(line, "\n")
switch diff.Type { for i := r.start; i <= r.end; i++ {
case diffmatchpatch.DiffDelete: l := lines[i]
buf.WriteString(fmt.Sprintf("-%s\n", cleanLine)) switch l.op {
case diffmatchpatch.DiffInsert:
buf.WriteString(fmt.Sprintf("+%s\n", cleanLine))
case diffmatchpatch.DiffEqual: case diffmatchpatch.DiffEqual:
buf.WriteString(fmt.Sprintf(" %s\n", cleanLine)) buf.WriteString(fmt.Sprintf(" %s\n", l.text))
case diffmatchpatch.DiffDelete:
buf.WriteString(fmt.Sprintf("-%s\n", l.text))
case diffmatchpatch.DiffInsert:
buf.WriteString(fmt.Sprintf("+%s\n", l.text))
} }
} }
} }
+5
View File
@@ -85,6 +85,9 @@ var DefaultServerConfigs = map[protocol.Language]ServerConfig{
protocol.LangCpp: { protocol.LangCpp: {
Command: []string{"clangd"}, Command: []string{"clangd"},
}, },
protocol.LangRust: {
Command: []string{"rust-analyzer"},
},
} }
// AllowedLSPBinaries is a whitelist of allowed LSP server binary names. // AllowedLSPBinaries is a whitelist of allowed LSP server binary names.
@@ -602,6 +605,8 @@ func languageToLSPID(lang protocol.Language) string {
return "c" return "c"
case protocol.LangCpp: case protocol.LangCpp:
return "cpp" return "cpp"
case protocol.LangRust:
return "rust"
default: default:
return string(lang) return string(lang)
} }
+60
View File
@@ -48,6 +48,8 @@ func ExtractDocComment(n *sitter.Node, content []byte, lang protocol.Language) *
return extractCDocComment(n, content) return extractCDocComment(n, content)
case protocol.LangElixir: case protocol.LangElixir:
return extractElixirDocComment(n, content) return extractElixirDocComment(n, content)
case protocol.LangRust:
return extractRustDocComment(n, content)
default: default:
return nil return nil
} }
@@ -551,6 +553,64 @@ func cleanPythonDocstring(doc string) string {
return strings.TrimSpace(doc) return strings.TrimSpace(doc)
} }
// extractRustDocComment extracts Rust documentation comments (/// style).
func extractRustDocComment(n *sitter.Node, content []byte) *DocComment {
comments := collectPrecedingComments(n, content, []string{"line_comment"})
if len(comments) == 0 {
return nil
}
// Filter for /// doc comments only
var docComments []*sitter.Node
for _, c := range comments {
text := GetNodeText(c, content)
trimmed := strings.TrimSpace(text)
if strings.HasPrefix(trimmed, "///") {
docComments = append(docComments, c)
}
}
if len(docComments) == 0 {
return nil
}
var parts []string
var raw []string
startLine := -1
endLine := -1
for _, c := range docComments {
text := GetNodeText(c, content)
raw = append(raw, text)
if startLine == -1 {
startLine = int(c.StartPoint().Row) + 1
}
endLine = int(c.EndPoint().Row) + 1
// Clean /// prefix
cleaned := strings.TrimSpace(text)
cleaned = strings.TrimPrefix(cleaned, "///")
if len(cleaned) > 0 && cleaned[0] == ' ' {
cleaned = cleaned[1:]
}
parts = append(parts, cleaned)
}
if len(parts) == 0 {
return nil
}
return &DocComment{
Text: strings.Join(parts, "\n"),
Raw: strings.Join(raw, "\n"),
Style: CommentStyleDoxygen,
Tags: nil,
StartLine: startLine,
EndLine: endLine,
}
}
// extractElixirDocComment extracts Elixir documentation from @doc and @moduledoc attributes. // extractElixirDocComment extracts Elixir documentation from @doc and @moduledoc attributes.
// Elixir uses module attributes like @doc and @moduledoc for documentation. // Elixir uses module attributes like @doc and @moduledoc for documentation.
func extractElixirDocComment(n *sitter.Node, content []byte) *DocComment { func extractElixirDocComment(n *sitter.Node, content []byte) *DocComment {
+4 -1
View File
@@ -18,6 +18,7 @@ import (
"github.com/smacker/go-tree-sitter/html" "github.com/smacker/go-tree-sitter/html"
"github.com/smacker/go-tree-sitter/javascript" "github.com/smacker/go-tree-sitter/javascript"
"github.com/smacker/go-tree-sitter/python" "github.com/smacker/go-tree-sitter/python"
"github.com/smacker/go-tree-sitter/rust"
"github.com/smacker/go-tree-sitter/typescript/typescript" "github.com/smacker/go-tree-sitter/typescript/typescript"
"github.com/lukaszraczylo/mcp-filepuff/pkg/errors" "github.com/lukaszraczylo/mcp-filepuff/pkg/errors"
@@ -117,10 +118,12 @@ func getLanguage(lang protocol.Language) (*sitter.Language, error) {
return html.GetLanguage(), nil return html.GetLanguage(), nil
case protocol.LangElixir: case protocol.LangElixir:
return elixir.GetLanguage(), nil return elixir.GetLanguage(), nil
case protocol.LangRust:
return rust.GetLanguage(), nil
default: default:
return nil, errors.New(errors.ErrInvalidLanguage, fmt.Sprintf("language %s is not supported", lang)). return nil, errors.New(errors.ErrInvalidLanguage, fmt.Sprintf("language %s is not supported", lang)).
WithContext("language", string(lang)). WithContext("language", string(lang)).
WithRemediation("Supported languages: Go, TypeScript, JavaScript, Python, C, C++, HTML, Vue, Elixir") WithRemediation("Supported languages: Go, TypeScript, JavaScript, Python, C, C++, HTML, Vue, Elixir, Rust")
} }
} }
+129
View File
@@ -27,6 +27,8 @@ func ExtractSymbols(tree *sitter.Tree, content []byte, lang protocol.Language, f
return extractCSymbols(root, content, filename) return extractCSymbols(root, content, filename)
case protocol.LangElixir: case protocol.LangElixir:
return extractElixirSymbols(root, content, filename) return extractElixirSymbols(root, content, filename)
case protocol.LangRust:
return extractRustSymbols(root, content, filename)
default: default:
return nil return nil
} }
@@ -714,6 +716,133 @@ func extractElixirProtocol(n *sitter.Node, content []byte, filename string) *pro
} }
} }
// extractRustSymbols extracts symbols from Rust code.
func extractRustSymbols(root *sitter.Node, content []byte, filename string) []protocol.Symbol {
var symbols []protocol.Symbol
WalkTree(root, func(n *sitter.Node) bool {
var symbol *protocol.Symbol
switch n.Type() {
case "function_item":
nameNode := n.ChildByFieldName("name")
if nameNode != nil {
symbol = &protocol.Symbol{
Name: GetNodeText(nameNode, content),
Kind: protocol.SymbolFunction,
Location: NodeLocation(n, filename),
}
}
case "struct_item":
nameNode := n.ChildByFieldName("name")
if nameNode != nil {
symbol = &protocol.Symbol{
Name: GetNodeText(nameNode, content),
Kind: protocol.SymbolStruct,
Location: NodeLocation(n, filename),
}
}
case "enum_item":
nameNode := n.ChildByFieldName("name")
if nameNode != nil {
symbol = &protocol.Symbol{
Name: GetNodeText(nameNode, content),
Kind: protocol.SymbolEnum,
Location: NodeLocation(n, filename),
}
}
case "trait_item":
nameNode := n.ChildByFieldName("name")
if nameNode != nil {
symbol = &protocol.Symbol{
Name: GetNodeText(nameNode, content),
Kind: protocol.SymbolTrait,
Location: NodeLocation(n, filename),
}
}
case "impl_item":
symbol = extractRustImpl(n, content, filename)
case "type_item":
nameNode := n.ChildByFieldName("name")
if nameNode != nil {
symbol = &protocol.Symbol{
Name: GetNodeText(nameNode, content),
Kind: protocol.SymbolType,
Location: NodeLocation(n, filename),
}
}
case "const_item":
nameNode := n.ChildByFieldName("name")
if nameNode != nil {
symbol = &protocol.Symbol{
Name: GetNodeText(nameNode, content),
Kind: protocol.SymbolConstant,
Location: NodeLocation(n, filename),
}
}
case "static_item":
nameNode := n.ChildByFieldName("name")
if nameNode != nil {
symbol = &protocol.Symbol{
Name: GetNodeText(nameNode, content),
Kind: protocol.SymbolVariable,
Location: NodeLocation(n, filename),
}
}
case "macro_definition":
nameNode := n.ChildByFieldName("name")
if nameNode != nil {
symbol = &protocol.Symbol{
Name: GetNodeText(nameNode, content) + " (macro)",
Kind: protocol.SymbolFunction,
Location: NodeLocation(n, filename),
}
}
case "mod_item":
nameNode := n.ChildByFieldName("name")
if nameNode != nil {
symbol = &protocol.Symbol{
Name: GetNodeText(nameNode, content),
Kind: protocol.SymbolModule,
Location: NodeLocation(n, filename),
}
}
}
if symbol != nil {
if doc := ExtractDocComment(n, content, protocol.LangRust); doc != nil {
symbol.Doc = FormatDocComment(doc)
}
symbols = append(symbols, *symbol)
}
return true
})
return symbols
}
// extractRustImpl extracts an impl block symbol from Rust code.
func extractRustImpl(n *sitter.Node, content []byte, filename string) *protocol.Symbol {
typeNode := n.ChildByFieldName("type")
traitNode := n.ChildByFieldName("trait")
var name string
if traitNode != nil && typeNode != nil {
name = "impl " + GetNodeText(traitNode, content) + " for " + GetNodeText(typeNode, content)
} else if typeNode != nil {
name = "impl " + GetNodeText(typeNode, content)
} else {
return nil
}
return &protocol.Symbol{
Name: name,
Kind: protocol.SymbolType,
Location: NodeLocation(n, filename),
}
}
// extractElixirImpl extracts a protocol implementation. // extractElixirImpl extracts a protocol implementation.
func extractElixirImpl(n *sitter.Node, content []byte, filename string) *protocol.Symbol { func extractElixirImpl(n *sitter.Node, content []byte, filename string) *protocol.Symbol {
// defimpl Protocol, for: Type do ... end // defimpl Protocol, for: Type do ... end
+7 -1
View File
@@ -56,7 +56,7 @@ func (s *Server) handleASTQuery(ctx context.Context, request mcp.CallToolRequest
// Find files to search based on language // Find files to search based on language
exts := languageToExtensions(language) exts := languageToExtensions(language)
if len(exts) == 0 { if len(exts) == 0 {
return mcp.NewToolResultError(fmt.Sprintf("unsupported language: %s (supported: go, typescript, javascript, python, c, cpp, html, vue, elixir)", language)), nil return mcp.NewToolResultError(fmt.Sprintf("unsupported language: %s (supported: go, typescript, javascript, python, c, cpp, html, vue, elixir, rust)", language)), nil
} }
var allResults []query.MatchResult var allResults []query.MatchResult
@@ -205,6 +205,10 @@ func symbolKindIcon(kind protocol.SymbolKind) string {
return "mod" return "mod"
case protocol.SymbolPackage: case protocol.SymbolPackage:
return "pkg" return "pkg"
case protocol.SymbolEnum:
return "enum"
case protocol.SymbolTrait:
return "trait"
default: default:
return "sym" return "sym"
} }
@@ -231,6 +235,8 @@ func languageToExtensions(language string) []string {
return []string{".vue"} return []string{".vue"}
case "elixir": case "elixir":
return []string{".ex", ".exs"} return []string{".ex", ".exs"}
case "rust":
return []string{".rs"}
default: default:
return nil return nil
} }
+50 -10
View File
@@ -89,7 +89,8 @@ func (s *Server) registerTools() {
// Register ping tool for health checks // Register ping tool for health checks
s.mcp.AddTool( s.mcp.AddTool(
mcp.NewTool("ping", mcp.NewTool("ping",
mcp.WithDescription("Health check - returns pong to verify the server is running"), mcp.WithDescription("Health check - returns pong to verify the server is running.\n\n"+
"Returns: \"pong\" text string."),
mcp.WithReadOnlyHintAnnotation(true), mcp.WithReadOnlyHintAnnotation(true),
), ),
s.handlePing, s.handlePing,
@@ -99,7 +100,10 @@ func (s *Server) registerTools() {
if s.searcher != nil { if s.searcher != nil {
s.mcp.AddTool( s.mcp.AddTool(
mcp.NewTool("file_search", mcp.NewTool("file_search",
mcp.WithDescription("Search for text patterns in files using ripgrep. Supports regex patterns, file type filtering, and context lines."), mcp.WithDescription("Search for text patterns in files using ripgrep. Supports regex patterns, file type filtering, and context lines.\n\n"+
"Returns: Results grouped by file with match context. Format: \"Found N matches in M files:\" followed by file sections, "+
"each with matching lines prefixed by \"L{line}│\" and context lines prefixed by \" │\".\n\n"+
"Example: {\"pattern\": \"func.*Error\", \"file_types\": [\"go\"], \"max_results\": 20}"),
mcp.WithReadOnlyHintAnnotation(true), mcp.WithReadOnlyHintAnnotation(true),
mcp.WithString("pattern", mcp.WithString("pattern",
mcp.Required(), mcp.Required(),
@@ -133,7 +137,16 @@ func (s *Server) registerTools() {
// Register file_read tool // Register file_read tool
s.mcp.AddTool( s.mcp.AddTool(
mcp.NewTool("file_read", mcp.NewTool("file_read",
mcp.WithDescription("Read a file's contents with optional line range and AST symbol summary"), mcp.WithDescription("Read a file's contents with optional line range and AST symbol summary.\n\n"+
"Returns: File content with numbered lines (format: \" 12│ line text\"). "+
"When include_ast=true: prepends symbol summary (\"**file.go** (N lines, go)\\nSymbols:\\n func Name L12\\n struct Config L45\"). "+
"When symbols_only=true: returns only the symbol summary (~95% fewer tokens). "+
"When max_lines is set: truncates output with \"[... N more lines omitted]\" notice.\n\n"+
"Examples:\n"+
" Full file: {\"path\": \"main.go\"}\n"+
" With AST: {\"path\": \"main.go\", \"include_ast\": true}\n"+
" Symbols only: {\"path\": \"main.go\", \"include_ast\": true, \"symbols_only\": true}\n"+
" Line range: {\"path\": \"main.go\", \"line_start\": 10, \"line_end\": 50}"),
mcp.WithReadOnlyHintAnnotation(true), mcp.WithReadOnlyHintAnnotation(true),
mcp.WithString("path", mcp.WithString("path",
mcp.Required(), mcp.Required(),
@@ -161,7 +174,13 @@ func (s *Server) registerTools() {
// Register ast_query tool // Register ast_query tool
s.mcp.AddTool( s.mcp.AddTool(
mcp.NewTool("ast_query", mcp.NewTool("ast_query",
mcp.WithDescription("Search for AST patterns in code files. Use code patterns with $VAR placeholders to match and capture code structures like functions, classes, and types."), mcp.WithDescription("Search for AST patterns in code files. Use code patterns with $VAR placeholders to match and capture code structures like functions, classes, and types.\n\n"+
"Returns: \"Found N match(es):\" followed by entries in format \"**file:line** (node_type)\" with code blocks "+
"and captured variables ($NAME=value). Returns \"No matches found.\" when no results.\n\n"+
"Examples:\n"+
" Go error funcs: {\"pattern\": \"func $NAME($$$ARGS) error\", \"language\": \"go\"}\n"+
" Python classes: {\"pattern\": \"class $NAME: $$$BODY\", \"language\": \"python\"}\n"+
" Named function: {\"pattern\": \"func $NAME($$$ARGS)\", \"language\": \"go\", \"name_exact\": \"NewServer\"}"),
mcp.WithReadOnlyHintAnnotation(true), mcp.WithReadOnlyHintAnnotation(true),
mcp.WithString("pattern", mcp.WithString("pattern",
mcp.Required(), mcp.Required(),
@@ -169,7 +188,7 @@ func (s *Server) registerTools() {
), ),
mcp.WithString("language", mcp.WithString("language",
mcp.Required(), mcp.Required(),
mcp.Description("Target language: go, typescript, javascript, python, c, cpp, html, vue, elixir"), mcp.Description("Target language: go, typescript, javascript, python, c, cpp, html, vue, elixir, rust"),
), ),
mcp.WithArray("paths", mcp.WithArray("paths",
mcp.Description("Paths to search in (defaults to workspace root)"), mcp.Description("Paths to search in (defaults to workspace root)"),
@@ -197,7 +216,10 @@ func (s *Server) registerTools() {
// Register symbol_at tool // Register symbol_at tool
s.mcp.AddTool( s.mcp.AddTool(
mcp.NewTool("symbol_at", mcp.NewTool("symbol_at",
mcp.WithDescription("Get information about the symbol at a specific position in a file. Returns type, documentation, and definition location using LSP when available."), mcp.WithDescription("Get information about the symbol at a specific position in a file. Returns type, documentation, and definition location using LSP when available.\n\n"+
"Returns: \"**Symbol Information**\" followed by hover/type information from LSP, or \"**Symbol Information** (AST fallback)\" "+
"with node type and text when LSP unavailable. Returns \"No symbol information available at this position.\" when nothing is found.\n\n"+
"Example: {\"file\": \"server.go\", \"line\": 45, \"column\": 6}"),
mcp.WithReadOnlyHintAnnotation(true), mcp.WithReadOnlyHintAnnotation(true),
mcp.WithString("file", mcp.WithString("file",
mcp.Required(), mcp.Required(),
@@ -218,7 +240,10 @@ func (s *Server) registerTools() {
// Register find_definition tool // Register find_definition tool
s.mcp.AddTool( s.mcp.AddTool(
mcp.NewTool("find_definition", mcp.NewTool("find_definition",
mcp.WithDescription("Find the definition of the symbol at a specific position. Uses LSP to locate where a function, variable, type, etc. is defined."), mcp.WithDescription("Find the definition of the symbol at a specific position. Uses LSP to locate where a function, variable, type, etc. is defined.\n\n"+
"Returns: \"Found N definition(s):\" with entries showing \"**file:line:column**\" and a 3-line code preview "+
"with the target line marked by \">\". Returns \"No definition found.\" when the symbol has no definition.\n\n"+
"Example: {\"file\": \"handler.go\", \"line\": 23, \"column\": 10}"),
mcp.WithReadOnlyHintAnnotation(true), mcp.WithReadOnlyHintAnnotation(true),
mcp.WithString("file", mcp.WithString("file",
mcp.Required(), mcp.Required(),
@@ -239,7 +264,10 @@ func (s *Server) registerTools() {
// Register find_references tool // Register find_references tool
s.mcp.AddTool( s.mcp.AddTool(
mcp.NewTool("find_references", mcp.NewTool("find_references",
mcp.WithDescription("Find all references to the symbol at a specific position. Uses LSP to locate all usages of a function, variable, type, etc."), mcp.WithDescription("Find all references to the symbol at a specific position. Uses LSP to locate all usages of a function, variable, type, etc.\n\n"+
"Returns: \"Found N reference(s):\" grouped by file, each showing \"**file** (count)\" with locations as "+
"\"L{line}:{column}\". Returns \"No references found.\" when no usages exist.\n\n"+
"Example: {\"file\": \"types.go\", \"line\": 5, \"column\": 6}"),
mcp.WithReadOnlyHintAnnotation(true), mcp.WithReadOnlyHintAnnotation(true),
mcp.WithString("file", mcp.WithString("file",
mcp.Required(), mcp.Required(),
@@ -264,7 +292,13 @@ func (s *Server) registerTools() {
// Register edit tools // Register edit tools
s.mcp.AddTool( s.mcp.AddTool(
mcp.NewTool("edit_preview", mcp.NewTool("edit_preview",
mcp.WithDescription("Preview an edit without applying it. Uses AST-aware editing for code files (Go, TypeScript, JavaScript, Python, C, C++), and text-based editing for other files (Markdown, JSON, YAML, config files, etc.)."), mcp.WithDescription("Preview an edit without applying it. Uses AST-aware editing for code files (Go, TypeScript, JavaScript, Python, C, C++, Rust), and text-based editing for other files (Markdown, JSON, YAML, config files, etc.).\n\n"+
"Returns: \"**Edit Preview**\" followed by a unified diff showing proposed changes. Does not modify the file. "+
"For code files: uses AST-aware mode with syntax validation. For other files: uses text-based mode.\n\n"+
"Examples:\n"+
" AST mode: {\"file\": \"main.go\", \"operation\": \"replace\", \"selector_kind\": \"function_declaration\", \"selector_name\": \"Hello\", \"new_content\": \"func Hello() {\\n\\treturn\\n}\"}\n"+
" Text mode: {\"file\": \"README.md\", \"operation\": \"replace\", \"selector_text\": \"## Old Header\", \"new_content\": \"## New Header\"}\n"+
" Line range: {\"file\": \"config.yaml\", \"operation\": \"replace\", \"selector_line\": 5, \"selector_line_end\": 10, \"new_content\": \"key: value\"}"),
mcp.WithString("file", mcp.WithString("file",
mcp.Required(), mcp.Required(),
mcp.Description("Path to the file to edit"), mcp.Description("Path to the file to edit"),
@@ -306,7 +340,13 @@ func (s *Server) registerTools() {
s.mcp.AddTool( s.mcp.AddTool(
mcp.NewTool("edit_apply", mcp.NewTool("edit_apply",
mcp.WithDescription("Apply an edit to a file. Uses AST-aware editing for code files (Go, TypeScript, JavaScript, Python, C, C++) with syntax validation, and text-based editing for other files (Markdown, JSON, YAML, config files, etc.)."), mcp.WithDescription("Apply an edit to a file. Uses AST-aware editing for code files (Go, TypeScript, JavaScript, Python, C, C++, Rust) with syntax validation, and text-based editing for other files (Markdown, JSON, YAML, config files, etc.).\n\n"+
"Returns: \"**Edit Applied Successfully**\" followed by a unified diff of the changes made. "+
"For code files, validates syntax before writing — returns an error if the edit would produce invalid syntax.\n\n"+
"Examples:\n"+
" AST mode: {\"file\": \"main.go\", \"operation\": \"replace\", \"selector_kind\": \"function_declaration\", \"selector_name\": \"Hello\", \"new_content\": \"func Hello() {\\n\\treturn\\n}\"}\n"+
" Text mode: {\"file\": \"README.md\", \"operation\": \"replace\", \"selector_text\": \"## Old Header\", \"new_content\": \"## New Header\"}\n"+
" Line range: {\"file\": \"config.yaml\", \"operation\": \"replace\", \"selector_line\": 5, \"selector_line_end\": 10, \"new_content\": \"key: value\"}"),
mcp.WithString("file", mcp.WithString("file",
mcp.Required(), mcp.Required(),
mcp.Description("Path to the file to edit"), mcp.Description("Path to the file to edit"),
+5
View File
@@ -32,6 +32,8 @@ const (
SymbolProperty SymbolKind = "property" SymbolProperty SymbolKind = "property"
SymbolModule SymbolKind = "module" SymbolModule SymbolKind = "module"
SymbolPackage SymbolKind = "package" SymbolPackage SymbolKind = "package"
SymbolEnum SymbolKind = "enum"
SymbolTrait SymbolKind = "trait"
) )
// Symbol represents a code symbol (function, class, variable, etc.). // Symbol represents a code symbol (function, class, variable, etc.).
@@ -63,6 +65,7 @@ const (
LangJSON Language = "json" LangJSON Language = "json"
LangYAML Language = "yaml" LangYAML Language = "yaml"
LangElixir Language = "elixir" LangElixir Language = "elixir"
LangRust Language = "rust"
LangUnknown Language = "unknown" LangUnknown Language = "unknown"
) )
@@ -92,6 +95,8 @@ func DetectLanguage(filename string) Language {
return LangYAML return LangYAML
case ".ex", ".exs": case ".ex", ".exs":
return LangElixir return LangElixir
case ".rs":
return LangRust
default: default:
return LangUnknown return LangUnknown
} }
+65
View File
@@ -0,0 +1,65 @@
/// A simple point in 2D space.
struct Point {
x: f64,
y: f64,
}
/// Represents different shapes.
enum Shape {
Circle(f64),
Rectangle(f64, f64),
Triangle(f64, f64, f64),
}
/// A trait for objects that can be drawn.
trait Drawable {
fn draw(&self);
fn area(&self) -> f64;
}
impl Drawable for Shape {
fn draw(&self) {
println!("Drawing shape");
}
fn area(&self) -> f64 {
0.0
}
}
impl Point {
/// Creates a new point.
fn new(x: f64, y: f64) -> Self {
Point { x, y }
}
fn distance(&self, other: &Point) -> f64 {
((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt()
}
}
/// A type alias for a list of points.
type PointList = Vec<Point>;
/// The maximum number of points allowed.
const MAX_POINTS: usize = 1000;
static ORIGIN: Point = Point { x: 0.0, y: 0.0 };
/// A helper macro for creating points.
macro_rules! point {
($x:expr, $y:expr) => {
Point::new($x, $y)
};
}
mod geometry {
pub fn hello() {
println!("Hello from geometry");
}
}
fn main() {
let p = point!(1.0, 2.0);
println!("Point: ({}, {})", p.x, p.y);
}