mirror of
https://github.com/lukaszraczylo/filepuff-mcp.git
synced 2026-06-05 22:23:50 +00:00
multi-fixes
This commit is contained in:
@@ -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.
|
||||
|
||||
### 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
|
||||
|
||||
### Running the Server (Standalone)
|
||||
|
||||
+18
-5
@@ -47,9 +47,10 @@ Health check - returns pong to verify the server is running
|
||||
{"tool": "ping"}
|
||||
```
|
||||
|
||||
**Returns:** `"pong"` text string.
|
||||
|
||||
**Notes:**
|
||||
|
||||
- Returns: "pong"
|
||||
- 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}
|
||||
```
|
||||
|
||||
**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:**
|
||||
|
||||
- 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}
|
||||
```
|
||||
|
||||
**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:**
|
||||
|
||||
- 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]"}
|
||||
```
|
||||
|
||||
**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:**
|
||||
|
||||
- $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}
|
||||
```
|
||||
|
||||
**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:**
|
||||
|
||||
- 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}
|
||||
```
|
||||
|
||||
**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:**
|
||||
|
||||
- Requires language server for the file type
|
||||
- Returns file path, line, and column of definition
|
||||
- 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}
|
||||
```
|
||||
|
||||
**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:**
|
||||
|
||||
- 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\\""}
|
||||
```
|
||||
|
||||
**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:**
|
||||
|
||||
- Returns a diff showing proposed changes
|
||||
- Does not modify the file
|
||||
- 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"}
|
||||
```
|
||||
|
||||
**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:**
|
||||
|
||||
- For code files: validates syntax before and after edit
|
||||
- Preserves file permissions
|
||||
- Uses atomic writes for safety
|
||||
- File locking prevents concurrent edits
|
||||
|
||||
+113
-22
@@ -174,8 +174,6 @@ func (e *Engine) performASTEdit(ctx context.Context, edit *ASTEdit, apply bool)
|
||||
result := &EditResult{
|
||||
Success: true,
|
||||
Diff: diff,
|
||||
OriginalContent: string(content),
|
||||
NewContent: string(newContent),
|
||||
Applied: false,
|
||||
}
|
||||
|
||||
@@ -233,8 +231,6 @@ func (e *Engine) performTextEdit(_ context.Context, edit *ASTEdit, apply bool) (
|
||||
result := &EditResult{
|
||||
Success: true,
|
||||
Diff: diff,
|
||||
OriginalContent: string(content),
|
||||
NewContent: string(newContent),
|
||||
Applied: false,
|
||||
}
|
||||
|
||||
@@ -568,14 +564,22 @@ func indentContent(content string, indent string) string {
|
||||
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.
|
||||
// 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 {
|
||||
dmp := e.dmp
|
||||
|
||||
// Use line-level diffing: encode each line as a single character,
|
||||
// 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)
|
||||
diffs := dmp.DiffMain(chars1, chars2, false)
|
||||
diffs = dmp.DiffCharsToLines(diffs, lineArray)
|
||||
@@ -583,30 +587,117 @@ func (e *Engine) generateDiff(original, modified, filename string) string {
|
||||
// Cleanup for readability
|
||||
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
|
||||
buf.WriteString(fmt.Sprintf("--- %s\n", filename))
|
||||
buf.WriteString(fmt.Sprintf("+++ %s\n", filename))
|
||||
|
||||
for _, diff := range diffs {
|
||||
// SplitAfter preserves the trailing \n on each line, so we can
|
||||
// distinguish real lines from a trailing empty split artifact.
|
||||
lines := strings.SplitAfter(diff.Text, "\n")
|
||||
for _, line := range lines {
|
||||
if line == "" {
|
||||
continue
|
||||
for _, r := range ranges {
|
||||
// Determine hunk header line numbers
|
||||
var oldStart, oldCount, newStart, newCount int
|
||||
for i := r.start; i <= r.end; i++ {
|
||||
l := lines[i]
|
||||
switch l.op {
|
||||
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.
|
||||
cleanLine := strings.TrimSuffix(line, "\n")
|
||||
buf.WriteString(fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", oldStart, oldCount, newStart, newCount))
|
||||
|
||||
switch diff.Type {
|
||||
case diffmatchpatch.DiffDelete:
|
||||
buf.WriteString(fmt.Sprintf("-%s\n", cleanLine))
|
||||
case diffmatchpatch.DiffInsert:
|
||||
buf.WriteString(fmt.Sprintf("+%s\n", cleanLine))
|
||||
for i := r.start; i <= r.end; i++ {
|
||||
l := lines[i]
|
||||
switch l.op {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +85,9 @@ var DefaultServerConfigs = map[protocol.Language]ServerConfig{
|
||||
protocol.LangCpp: {
|
||||
Command: []string{"clangd"},
|
||||
},
|
||||
protocol.LangRust: {
|
||||
Command: []string{"rust-analyzer"},
|
||||
},
|
||||
}
|
||||
|
||||
// AllowedLSPBinaries is a whitelist of allowed LSP server binary names.
|
||||
@@ -602,6 +605,8 @@ func languageToLSPID(lang protocol.Language) string {
|
||||
return "c"
|
||||
case protocol.LangCpp:
|
||||
return "cpp"
|
||||
case protocol.LangRust:
|
||||
return "rust"
|
||||
default:
|
||||
return string(lang)
|
||||
}
|
||||
|
||||
@@ -48,6 +48,8 @@ func ExtractDocComment(n *sitter.Node, content []byte, lang protocol.Language) *
|
||||
return extractCDocComment(n, content)
|
||||
case protocol.LangElixir:
|
||||
return extractElixirDocComment(n, content)
|
||||
case protocol.LangRust:
|
||||
return extractRustDocComment(n, content)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
@@ -551,6 +553,64 @@ func cleanPythonDocstring(doc string) string {
|
||||
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.
|
||||
// Elixir uses module attributes like @doc and @moduledoc for documentation.
|
||||
func extractElixirDocComment(n *sitter.Node, content []byte) *DocComment {
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/smacker/go-tree-sitter/html"
|
||||
"github.com/smacker/go-tree-sitter/javascript"
|
||||
"github.com/smacker/go-tree-sitter/python"
|
||||
"github.com/smacker/go-tree-sitter/rust"
|
||||
"github.com/smacker/go-tree-sitter/typescript/typescript"
|
||||
|
||||
"github.com/lukaszraczylo/mcp-filepuff/pkg/errors"
|
||||
@@ -117,10 +118,12 @@ func getLanguage(lang protocol.Language) (*sitter.Language, error) {
|
||||
return html.GetLanguage(), nil
|
||||
case protocol.LangElixir:
|
||||
return elixir.GetLanguage(), nil
|
||||
case protocol.LangRust:
|
||||
return rust.GetLanguage(), nil
|
||||
default:
|
||||
return nil, errors.New(errors.ErrInvalidLanguage, fmt.Sprintf("language %s is not supported", 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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ func ExtractSymbols(tree *sitter.Tree, content []byte, lang protocol.Language, f
|
||||
return extractCSymbols(root, content, filename)
|
||||
case protocol.LangElixir:
|
||||
return extractElixirSymbols(root, content, filename)
|
||||
case protocol.LangRust:
|
||||
return extractRustSymbols(root, content, filename)
|
||||
default:
|
||||
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.
|
||||
func extractElixirImpl(n *sitter.Node, content []byte, filename string) *protocol.Symbol {
|
||||
// defimpl Protocol, for: Type do ... end
|
||||
|
||||
@@ -56,7 +56,7 @@ func (s *Server) handleASTQuery(ctx context.Context, request mcp.CallToolRequest
|
||||
// Find files to search based on language
|
||||
exts := languageToExtensions(language)
|
||||
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
|
||||
@@ -205,6 +205,10 @@ func symbolKindIcon(kind protocol.SymbolKind) string {
|
||||
return "mod"
|
||||
case protocol.SymbolPackage:
|
||||
return "pkg"
|
||||
case protocol.SymbolEnum:
|
||||
return "enum"
|
||||
case protocol.SymbolTrait:
|
||||
return "trait"
|
||||
default:
|
||||
return "sym"
|
||||
}
|
||||
@@ -231,6 +235,8 @@ func languageToExtensions(language string) []string {
|
||||
return []string{".vue"}
|
||||
case "elixir":
|
||||
return []string{".ex", ".exs"}
|
||||
case "rust":
|
||||
return []string{".rs"}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
+50
-10
@@ -89,7 +89,8 @@ func (s *Server) registerTools() {
|
||||
// Register ping tool for health checks
|
||||
s.mcp.AddTool(
|
||||
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),
|
||||
),
|
||||
s.handlePing,
|
||||
@@ -99,7 +100,10 @@ func (s *Server) registerTools() {
|
||||
if s.searcher != nil {
|
||||
s.mcp.AddTool(
|
||||
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.WithString("pattern",
|
||||
mcp.Required(),
|
||||
@@ -133,7 +137,16 @@ func (s *Server) registerTools() {
|
||||
// Register file_read tool
|
||||
s.mcp.AddTool(
|
||||
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.WithString("path",
|
||||
mcp.Required(),
|
||||
@@ -161,7 +174,13 @@ func (s *Server) registerTools() {
|
||||
// Register ast_query tool
|
||||
s.mcp.AddTool(
|
||||
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.WithString("pattern",
|
||||
mcp.Required(),
|
||||
@@ -169,7 +188,7 @@ func (s *Server) registerTools() {
|
||||
),
|
||||
mcp.WithString("language",
|
||||
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.Description("Paths to search in (defaults to workspace root)"),
|
||||
@@ -197,7 +216,10 @@ func (s *Server) registerTools() {
|
||||
// Register symbol_at tool
|
||||
s.mcp.AddTool(
|
||||
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.WithString("file",
|
||||
mcp.Required(),
|
||||
@@ -218,7 +240,10 @@ func (s *Server) registerTools() {
|
||||
// Register find_definition tool
|
||||
s.mcp.AddTool(
|
||||
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.WithString("file",
|
||||
mcp.Required(),
|
||||
@@ -239,7 +264,10 @@ func (s *Server) registerTools() {
|
||||
// Register find_references tool
|
||||
s.mcp.AddTool(
|
||||
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.WithString("file",
|
||||
mcp.Required(),
|
||||
@@ -264,7 +292,13 @@ func (s *Server) registerTools() {
|
||||
// Register edit tools
|
||||
s.mcp.AddTool(
|
||||
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.Required(),
|
||||
mcp.Description("Path to the file to edit"),
|
||||
@@ -306,7 +340,13 @@ func (s *Server) registerTools() {
|
||||
|
||||
s.mcp.AddTool(
|
||||
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.Required(),
|
||||
mcp.Description("Path to the file to edit"),
|
||||
|
||||
@@ -32,6 +32,8 @@ const (
|
||||
SymbolProperty SymbolKind = "property"
|
||||
SymbolModule SymbolKind = "module"
|
||||
SymbolPackage SymbolKind = "package"
|
||||
SymbolEnum SymbolKind = "enum"
|
||||
SymbolTrait SymbolKind = "trait"
|
||||
)
|
||||
|
||||
// Symbol represents a code symbol (function, class, variable, etc.).
|
||||
@@ -63,6 +65,7 @@ const (
|
||||
LangJSON Language = "json"
|
||||
LangYAML Language = "yaml"
|
||||
LangElixir Language = "elixir"
|
||||
LangRust Language = "rust"
|
||||
LangUnknown Language = "unknown"
|
||||
)
|
||||
|
||||
@@ -92,6 +95,8 @@ func DetectLanguage(filename string) Language {
|
||||
return LangYAML
|
||||
case ".ex", ".exs":
|
||||
return LangElixir
|
||||
case ".rs":
|
||||
return LangRust
|
||||
default:
|
||||
return LangUnknown
|
||||
}
|
||||
|
||||
Vendored
+65
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user