Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ This is an [MCP](https://modelcontextprotocol.io/introduction) server that runs
## Tools

- `definition`: Retrieves the complete source code definition of any symbol (function, type, constant, etc.) from your codebase.
- `content`: Retrieves the complete source code definition (function, type, constant, etc.) from your codebase at a specific location.
- `references`: Locates all usages and references of a symbol throughout the codebase.
- `diagnostics`: Provides diagnostic information for a specific file, including warnings and errors.
- `hover`: Display documentation, type hints, or other hover information for a given location.
Expand Down
7 changes: 7 additions & 0 deletions integrationtests/snapshots/go/content/test_function.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Symbol: TestFunction
/TEST_OUTPUT/workspace/clean.go
Range: L31:C1 - L33:C2

31|func TestFunction() {
32| fmt.Println("This is a test function")
33|}
56 changes: 56 additions & 0 deletions integrationtests/tests/go/content/content_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package content_test

import (
"context"
"path/filepath"
"strings"
"testing"
"time"

"github.com/isaacphi/mcp-language-server/integrationtests/tests/common"
"github.com/isaacphi/mcp-language-server/integrationtests/tests/go/internal"
"github.com/isaacphi/mcp-language-server/internal/tools"
)

func TestContent(t *testing.T) {
suite := internal.GetTestSuite(t)

ctx, cancel := context.WithTimeout(suite.Context, 10*time.Second)
defer cancel()

tests := []struct {
name string
file string
line int
column int
expectedText string
snapshotName string
}{
{
name: "Function",
file: filepath.Join(suite.WorkspaceDir, "clean.go"),
line: 32,
column: 1,
expectedText: "func TestFunction()",
snapshotName: "test_function",
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Call the ReadDefinition tool
result, err := tools.GetContentInfo(ctx, suite.Client, tc.file, tc.line, tc.column)
if err != nil {
t.Fatalf("Failed to read content: %v", err)
}

// Check that the result contains relevant information
if !strings.Contains(result, tc.expectedText) {
t.Errorf("Content does not contain expected text: %s", tc.expectedText)
}

// Use snapshot testing to verify exact output
common.SnapshotTest(t, "go", "content", tc.snapshotName, result)
})
}
}
54 changes: 54 additions & 0 deletions internal/tools/content.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package tools

import (
"context"
"fmt"
"strings"

"github.com/isaacphi/mcp-language-server/internal/lsp"
"github.com/isaacphi/mcp-language-server/internal/protocol"
)

// GetContentInfo reads the source code definition of a symbol (function, type, constant, etc.) at the specified position
func GetContentInfo(ctx context.Context, client *lsp.Client, filePath string, line, column int) (string, error) {
// Open the file if not already open
err := client.OpenFile(ctx, filePath)
if err != nil {
return "", fmt.Errorf("could not open file: %v", err)
}

// Convert 1-indexed line/column to 0-indexed for LSP protocol
position := protocol.Position{
Line: uint32(line - 1),
Character: uint32(column - 1),
}

location := protocol.Location{
URI: protocol.DocumentUri("file://" + filePath),
Range: protocol.Range{
Start: position,
End: position,
},
}

definition, loc, symbol, err := GetFullDefinition(ctx, client, location)
locationInfo := fmt.Sprintf(
"Symbol: %s\n"+
"File: %s\n"+
"Range: L%d:C%d - L%d:C%d\n\n",
symbol.GetName(),
strings.TrimPrefix(string(loc.URI), "file://"),
loc.Range.Start.Line+1,
loc.Range.Start.Character+1,
loc.Range.End.Line+1,
loc.Range.End.Character+1,
)

if err != nil {
return "", err
}

definition = addLineNumbers(definition, int(loc.Range.Start.Line)+1)

return locationInfo + definition, nil
}
2 changes: 1 addition & 1 deletion internal/tools/definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func ReadDefinition(ctx context.Context, client *lsp.Client, symbolName string)
}

banner := "---\n\n"
definition, loc, err := GetFullDefinition(ctx, client, loc)
definition, loc, _, err := GetFullDefinition(ctx, client, loc)
locationInfo := fmt.Sprintf(
"Symbol: %s\n"+
"File: %s\n"+
Expand Down
22 changes: 12 additions & 10 deletions internal/tools/lsp-utilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
)

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

symbols, err := symResult.Results()
if err != nil {
return "", protocol.Location{}, fmt.Errorf("failed to process document symbols: %w", err)
return "", protocol.Location{}, nil, fmt.Errorf("failed to process document symbols: %w", err)
}

var symbolRange protocol.Range
var symbol protocol.DocumentSymbolResult
found := false

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

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

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

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

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

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

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

return "", protocol.Location{}, fmt.Errorf("symbol not found")
return "", protocol.Location{}, nil, fmt.Errorf("symbol not found")
}

// GetLineRangesToDisplay determines which lines should be displayed for a set of locations
Expand All @@ -146,7 +148,7 @@ func GetLineRangesToDisplay(ctx context.Context, client *lsp.Client, locations [
// For each location, get its container and add relevant lines
for _, loc := range locations {
// Use GetFullDefinition to find container
_, containerLoc, err := GetFullDefinition(ctx, client, loc)
_, containerLoc, _, err := GetFullDefinition(ctx, client, loc)
if err != nil {
// If container not found, just use the location's line
refLine := int(loc.Range.Start.Line)
Expand Down
42 changes: 42 additions & 0 deletions tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,48 @@ func (s *mcpServer) registerTools() error {
return mcp.NewToolResultText(text), nil
})

contentTool := mcp.NewTool("content",
mcp.WithDescription("Read the source code definition of a symbol (function, type, constant, etc.) at the specified location."),
mcp.WithString("filePath",
mcp.Required(),
mcp.Description("The path to the file"),
),
mcp.WithNumber("line",
mcp.Required(),
mcp.Description("The line number where the content is requested (1-indexed)"),
),
mcp.WithNumber("column",
mcp.Required(),
mcp.Description("The column number where the content is requested (1-indexed)"),
),
)

s.mcpServer.AddTool(contentTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// Extract arguments
filePath, err := request.RequireString("filePath")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}

line, err := request.RequireInt("line")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}

column, err := request.RequireInt("column")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}

coreLogger.Debug("Executing content for file: %s line: %d column: %d", filePath, line, column)
text, err := tools.GetContentInfo(s.ctx, s.lspClient, filePath, line, column)
if err != nil {
coreLogger.Error("Failed to get content information: %v", err)
return mcp.NewToolResultError(fmt.Sprintf("failed to get content: %v", err)), nil
}
return mcp.NewToolResultText(text), nil
})

coreLogger.Info("Successfully registered all MCP tools")
return nil
}
Loading