Skip to content

Commit 96b30b6

Browse files
authored
Merge pull request isaacphi#10 from virtuald/content
Add content tool
2 parents c261e7c + baf444b commit 96b30b6

File tree

7 files changed

+173
-11
lines changed

7 files changed

+173
-11
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ This is an [MCP](https://modelcontextprotocol.io/introduction) server that runs
173173
## Tools
174174

175175
- `definition`: Retrieves the complete source code definition of any symbol (function, type, constant, etc.) from your codebase.
176+
- `content`: Retrieves the complete source code definition (function, type, constant, etc.) from your codebase at a specific location.
176177
- `references`: Locates all usages and references of a symbol throughout the codebase.
177178
- `diagnostics`: Provides diagnostic information for a specific file, including warnings and errors.
178179
- `hover`: Display documentation, type hints, or other hover information for a given location.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Symbol: TestFunction
2+
/TEST_OUTPUT/workspace/clean.go
3+
Range: L31:C1 - L33:C2
4+
5+
31|func TestFunction() {
6+
32| fmt.Println("This is a test function")
7+
33|}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package content_test
2+
3+
import (
4+
"context"
5+
"path/filepath"
6+
"strings"
7+
"testing"
8+
"time"
9+
10+
"github.com/isaacphi/mcp-language-server/integrationtests/tests/common"
11+
"github.com/isaacphi/mcp-language-server/integrationtests/tests/go/internal"
12+
"github.com/isaacphi/mcp-language-server/internal/tools"
13+
)
14+
15+
func TestContent(t *testing.T) {
16+
suite := internal.GetTestSuite(t)
17+
18+
ctx, cancel := context.WithTimeout(suite.Context, 10*time.Second)
19+
defer cancel()
20+
21+
tests := []struct {
22+
name string
23+
file string
24+
line int
25+
column int
26+
expectedText string
27+
snapshotName string
28+
}{
29+
{
30+
name: "Function",
31+
file: filepath.Join(suite.WorkspaceDir, "clean.go"),
32+
line: 32,
33+
column: 1,
34+
expectedText: "func TestFunction()",
35+
snapshotName: "test_function",
36+
},
37+
}
38+
39+
for _, tc := range tests {
40+
t.Run(tc.name, func(t *testing.T) {
41+
// Call the ReadDefinition tool
42+
result, err := tools.GetContentInfo(ctx, suite.Client, tc.file, tc.line, tc.column)
43+
if err != nil {
44+
t.Fatalf("Failed to read content: %v", err)
45+
}
46+
47+
// Check that the result contains relevant information
48+
if !strings.Contains(result, tc.expectedText) {
49+
t.Errorf("Content does not contain expected text: %s", tc.expectedText)
50+
}
51+
52+
// Use snapshot testing to verify exact output
53+
common.SnapshotTest(t, "go", "content", tc.snapshotName, result)
54+
})
55+
}
56+
}

internal/tools/content.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package tools
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/isaacphi/mcp-language-server/internal/lsp"
9+
"github.com/isaacphi/mcp-language-server/internal/protocol"
10+
)
11+
12+
// GetContentInfo reads the source code definition of a symbol (function, type, constant, etc.) at the specified position
13+
func GetContentInfo(ctx context.Context, client *lsp.Client, filePath string, line, column int) (string, error) {
14+
// Open the file if not already open
15+
err := client.OpenFile(ctx, filePath)
16+
if err != nil {
17+
return "", fmt.Errorf("could not open file: %v", err)
18+
}
19+
20+
// Convert 1-indexed line/column to 0-indexed for LSP protocol
21+
position := protocol.Position{
22+
Line: uint32(line - 1),
23+
Character: uint32(column - 1),
24+
}
25+
26+
location := protocol.Location{
27+
URI: protocol.DocumentUri("file://" + filePath),
28+
Range: protocol.Range{
29+
Start: position,
30+
End: position,
31+
},
32+
}
33+
34+
definition, loc, symbol, err := GetFullDefinition(ctx, client, location)
35+
locationInfo := fmt.Sprintf(
36+
"Symbol: %s\n"+
37+
"File: %s\n"+
38+
"Range: L%d:C%d - L%d:C%d\n\n",
39+
symbol.GetName(),
40+
strings.TrimPrefix(string(loc.URI), "file://"),
41+
loc.Range.Start.Line+1,
42+
loc.Range.Start.Character+1,
43+
loc.Range.End.Line+1,
44+
loc.Range.End.Character+1,
45+
)
46+
47+
if err != nil {
48+
return "", err
49+
}
50+
51+
definition = addLineNumbers(definition, int(loc.Range.Start.Line)+1)
52+
53+
return locationInfo + definition, nil
54+
}

internal/tools/definition.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ func ReadDefinition(ctx context.Context, client *lsp.Client, symbolName string)
6464
}
6565

6666
banner := "---\n\n"
67-
definition, loc, err := GetFullDefinition(ctx, client, loc)
67+
definition, loc, _, err := GetFullDefinition(ctx, client, loc)
6868
locationInfo := fmt.Sprintf(
6969
"Symbol: %s\n"+
7070
"File: %s\n"+

internal/tools/lsp-utilities.go

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212
)
1313

1414
// Gets the full code block surrounding the start of the input location
15-
func GetFullDefinition(ctx context.Context, client *lsp.Client, startLocation protocol.Location) (string, protocol.Location, error) {
15+
func GetFullDefinition(ctx context.Context, client *lsp.Client, startLocation protocol.Location) (string, protocol.Location, protocol.DocumentSymbolResult, error) {
1616
symParams := protocol.DocumentSymbolParams{
1717
TextDocument: protocol.TextDocumentIdentifier{
1818
URI: startLocation.URI,
@@ -22,22 +22,24 @@ func GetFullDefinition(ctx context.Context, client *lsp.Client, startLocation pr
2222
// Get all symbols in document
2323
symResult, err := client.DocumentSymbol(ctx, symParams)
2424
if err != nil {
25-
return "", protocol.Location{}, fmt.Errorf("failed to get document symbols: %w", err)
25+
return "", protocol.Location{}, nil, fmt.Errorf("failed to get document symbols: %w", err)
2626
}
2727

2828
symbols, err := symResult.Results()
2929
if err != nil {
30-
return "", protocol.Location{}, fmt.Errorf("failed to process document symbols: %w", err)
30+
return "", protocol.Location{}, nil, fmt.Errorf("failed to process document symbols: %w", err)
3131
}
3232

3333
var symbolRange protocol.Range
34+
var symbol protocol.DocumentSymbolResult
3435
found := false
3536

3637
// Search for symbol at startLocation
3738
var searchSymbols func(symbols []protocol.DocumentSymbolResult) bool
3839
searchSymbols = func(symbols []protocol.DocumentSymbolResult) bool {
3940
for _, sym := range symbols {
4041
if containsPosition(sym.GetRange(), startLocation.Range.Start) {
42+
symbol = sym
4143
symbolRange = sym.GetRange()
4244
found = true
4345
return true
@@ -62,14 +64,14 @@ func GetFullDefinition(ctx context.Context, client *lsp.Client, startLocation pr
6264
// Convert URI to filesystem path
6365
filePath, err := url.PathUnescape(strings.TrimPrefix(string(startLocation.URI), "file://"))
6466
if err != nil {
65-
return "", protocol.Location{}, fmt.Errorf("failed to unescape URI: %w", err)
67+
return "", protocol.Location{}, nil, fmt.Errorf("failed to unescape URI: %w", err)
6668
}
6769

6870
// Read the file to get the full lines of the definition
6971
// because we may have a start and end column
7072
content, err := os.ReadFile(filePath)
7173
if err != nil {
72-
return "", protocol.Location{}, fmt.Errorf("failed to read file: %w", err)
74+
return "", protocol.Location{}, nil, fmt.Errorf("failed to read file: %w", err)
7375
}
7476

7577
lines := strings.Split(string(content), "\n")
@@ -79,7 +81,7 @@ func GetFullDefinition(ctx context.Context, client *lsp.Client, startLocation pr
7981

8082
// Get the line at the end of the range
8183
if int(symbolRange.End.Line) >= len(lines) {
82-
return "", protocol.Location{}, fmt.Errorf("line number out of range")
84+
return "", protocol.Location{}, nil, fmt.Errorf("line number out of range")
8385
}
8486

8587
line := lines[symbolRange.End.Line]
@@ -128,14 +130,14 @@ func GetFullDefinition(ctx context.Context, client *lsp.Client, startLocation pr
128130

129131
// Return the text within the range
130132
if int(symbolRange.End.Line) >= len(lines) {
131-
return "", protocol.Location{}, fmt.Errorf("end line out of range")
133+
return "", protocol.Location{}, nil, fmt.Errorf("end line out of range")
132134
}
133135

134136
selectedLines := lines[symbolRange.Start.Line : symbolRange.End.Line+1]
135-
return strings.Join(selectedLines, "\n"), startLocation, nil
137+
return strings.Join(selectedLines, "\n"), startLocation, symbol, nil
136138
}
137139

138-
return "", protocol.Location{}, fmt.Errorf("symbol not found")
140+
return "", protocol.Location{}, nil, fmt.Errorf("symbol not found")
139141
}
140142

141143
// GetLineRangesToDisplay determines which lines should be displayed for a set of locations
@@ -146,7 +148,7 @@ func GetLineRangesToDisplay(ctx context.Context, client *lsp.Client, locations [
146148
// For each location, get its container and add relevant lines
147149
for _, loc := range locations {
148150
// Use GetFullDefinition to find container
149-
_, containerLoc, err := GetFullDefinition(ctx, client, loc)
151+
_, containerLoc, _, err := GetFullDefinition(ctx, client, loc)
150152
if err != nil {
151153
// If container not found, just use the location's line
152154
refLine := int(loc.Range.Start.Line)

tools.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,48 @@ func (s *mcpServer) registerTools() error {
382382
return mcp.NewToolResultText(text), nil
383383
})
384384

385+
contentTool := mcp.NewTool("content",
386+
mcp.WithDescription("Read the source code definition of a symbol (function, type, constant, etc.) at the specified location."),
387+
mcp.WithString("filePath",
388+
mcp.Required(),
389+
mcp.Description("The path to the file"),
390+
),
391+
mcp.WithNumber("line",
392+
mcp.Required(),
393+
mcp.Description("The line number where the content is requested (1-indexed)"),
394+
),
395+
mcp.WithNumber("column",
396+
mcp.Required(),
397+
mcp.Description("The column number where the content is requested (1-indexed)"),
398+
),
399+
)
400+
401+
s.mcpServer.AddTool(contentTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
402+
// Extract arguments
403+
filePath, err := request.RequireString("filePath")
404+
if err != nil {
405+
return mcp.NewToolResultError(err.Error()), nil
406+
}
407+
408+
line, err := request.RequireInt("line")
409+
if err != nil {
410+
return mcp.NewToolResultError(err.Error()), nil
411+
}
412+
413+
column, err := request.RequireInt("column")
414+
if err != nil {
415+
return mcp.NewToolResultError(err.Error()), nil
416+
}
417+
418+
coreLogger.Debug("Executing content for file: %s line: %d column: %d", filePath, line, column)
419+
text, err := tools.GetContentInfo(s.ctx, s.lspClient, filePath, line, column)
420+
if err != nil {
421+
coreLogger.Error("Failed to get content information: %v", err)
422+
return mcp.NewToolResultError(fmt.Sprintf("failed to get content: %v", err)), nil
423+
}
424+
return mcp.NewToolResultText(text), nil
425+
})
426+
385427
coreLogger.Info("Successfully registered all MCP tools")
386428
return nil
387429
}

0 commit comments

Comments
 (0)