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
16 changes: 16 additions & 0 deletions pkg/mcp/msi/filesystem.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ var ReadFile = &mcp.Tool{
Description: `Reads and returns the content of a specified file.`,
}

type ReadFileResult struct {
Content string `json:"content" jsonschema:"The content of the file."`
}

type ReadFileParams struct {
Path string `json:"path" jsonschema:"The absolute path to the file to read."`
// TODO: Offset *int `json:"offset,omitempty" jsonschema:"For text files, the 0-based line number to start reading from. Requires limit to be set."`
Expand All @@ -53,6 +57,10 @@ var WriteFile = &mcp.Tool{
Description: `Writes content to a specified file. If the file exists, it will be overwritten. If the file doesn't exist, it (and any necessary parent directories) will be created.`,
}

type WriteFileResult struct {
// Empty for now
}

type WriteFileParams struct {
Path string `json:"path" jsonschema:"The absolute path to the file to write to."`
Content string `json:"content" jsonschema:"The content to write into the file."`
Expand All @@ -69,6 +77,10 @@ type GlobParams struct {
// TODO: CaseSensitive bool `json:"case_sensitive,omitempty" jsonschema:": Whether the search should be case-sensitive. Defaults to false."`
}

type GlobResult struct {
Matches []string `json:"matches" jsonschema:"A list of absolute file paths that match the provided glob pattern."`
}

var SearchFileContent = &mcp.Tool{
Name: "search_file_content",
Description: `Searches for a regular expression pattern within the content of files in a specified directory. Internally calls 'git grep -n --no-index'.`,
Expand All @@ -80,4 +92,8 @@ type SearchFileContentParams struct {
Include *string `json:"include,omitempty" jsonschema:"A glob pattern to filter which files are searched (e.g., '*.js', 'src/**/*.{ts,tsx}'). If omitted, searches most files (respecting common ignores)."`
}

type SearchFileContentResult struct {
GitGrepOutput string `json:"git_grep_output" jsonschema:"The raw output from the 'git grep -n --no-index' command, containing matching lines with filenames and line numbers."`
}

// TODO: implement Replace
58 changes: 34 additions & 24 deletions pkg/mcp/toolset/filesystem.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ package toolset

import (
"context"
"encoding/json"
"errors"
"io"
"os"
Expand All @@ -19,7 +18,7 @@ import (

func (ts *ToolSet) ListDirectory(ctx context.Context,
_ *mcp.CallToolRequest, args msi.ListDirectoryParams,
) (*mcp.CallToolResult, any, error) {
) (*mcp.CallToolResult, *msi.ListDirectoryResult, error) {
if ts.inst == nil {
return nil, nil, errors.New("instance not registered")
}
Expand All @@ -31,7 +30,7 @@ func (ts *ToolSet) ListDirectory(ctx context.Context,
if err != nil {
return nil, nil, err
}
res := msi.ListDirectoryResult{
res := &msi.ListDirectoryResult{
Entries: make([]msi.ListDirectoryResultEntry, len(guestEnts)),
}
for i, f := range guestEnts {
Expand All @@ -41,18 +40,14 @@ func (ts *ToolSet) ListDirectory(ctx context.Context,
res.Entries[i].ModTime = ptr.Of(f.ModTime())
res.Entries[i].IsDir = ptr.Of(f.IsDir())
}
resJ, err := json.Marshal(res)
if err != nil {
return nil, nil, err
}
return &mcp.CallToolResult{
Content: []mcp.Content{&mcp.TextContent{Text: string(resJ)}},
}, nil, nil
StructuredContent: res,
}, res, nil
}

func (ts *ToolSet) ReadFile(_ context.Context,
_ *mcp.CallToolRequest, args msi.ReadFileParams,
) (*mcp.CallToolResult, any, error) {
) (*mcp.CallToolResult, *msi.ReadFileResult, error) {
if ts.inst == nil {
return nil, nil, errors.New("instance not registered")
}
Expand All @@ -71,17 +66,20 @@ func (ts *ToolSet) ReadFile(_ context.Context,
if err != nil {
return nil, nil, err
}
res := &msi.ReadFileResult{
Content: string(b),
}
return &mcp.CallToolResult{
// Gemini:
// For text files: The file content, potentially prefixed with a truncation message
// (e.g., [File content truncated: showing lines 1-100 of 500 total lines...]\nActual file content...).
Content: []mcp.Content{&mcp.TextContent{Text: string(b)}},
}, nil, nil
StructuredContent: res,
}, res, nil
}

func (ts *ToolSet) WriteFile(_ context.Context,
_ *mcp.CallToolRequest, args msi.WriteFileParams,
) (*mcp.CallToolResult, any, error) {
) (*mcp.CallToolResult, *msi.WriteFileResult, error) {
if ts.inst == nil {
return nil, nil, errors.New("instance not registered")
}
Expand All @@ -98,17 +96,18 @@ func (ts *ToolSet) WriteFile(_ context.Context,
if err != nil {
return nil, nil, err
}
res := &msi.WriteFileResult{}
return &mcp.CallToolResult{
// Gemini:
// A success message, e.g., `Successfully overwrote file: /path/to/your/file.txt`
// or `Successfully created and wrote to new file: /path/to/new/file.txt.`
Content: []mcp.Content{&mcp.TextContent{Text: "OK"}},
}, nil, nil
StructuredContent: res,
}, res, nil
}

func (ts *ToolSet) Glob(_ context.Context,
_ *mcp.CallToolRequest, args msi.GlobParams,
) (*mcp.CallToolResult, any, error) {
) (*mcp.CallToolResult, *msi.GlobResult, error) {
if ts.inst == nil {
return nil, nil, errors.New("instance not registered")
}
Expand All @@ -128,20 +127,19 @@ func (ts *ToolSet) Glob(_ context.Context,
if err != nil {
return nil, nil, err
}
resJ, err := json.Marshal(matches)
if err != nil {
return nil, nil, err
res := &msi.GlobResult{
Matches: matches,
}
return &mcp.CallToolResult{
// Gemini:
// A message like: Found 5 file(s) matching "*.ts" within src, sorted by modification time (newest first):\nsrc/file1.ts\nsrc/subdir/file2.ts...
Content: []mcp.Content{&mcp.TextContent{Text: string(resJ)}},
}, nil, nil
StructuredContent: res,
}, res, nil
}

func (ts *ToolSet) SearchFileContent(ctx context.Context,
req *mcp.CallToolRequest, args msi.SearchFileContentParams,
) (*mcp.CallToolResult, any, error) {
) (*mcp.CallToolResult, *msi.SearchFileContentResult, error) {
if ts.inst == nil {
return nil, nil, errors.New("instance not registered")
}
Expand All @@ -159,7 +157,19 @@ func (ts *ToolSet) SearchFileContent(ctx context.Context,
if args.Include != nil && *args.Include != "" {
guestPath = path.Join(guestPath, *args.Include)
}
return ts.RunShellCommand(ctx, req, msi.RunShellCommandParams{
Command: []string{"git", "grep", "-n", "--no-index", args.Pattern, guestPath},
cmdToolRes, cmdRes, err := ts.RunShellCommand(ctx, req, msi.RunShellCommandParams{
Command: []string{"git", "grep", "-n", "--no-index", args.Pattern, guestPath},
Directory: pathStr, // Directory must be always set
})
if err != nil {
return cmdToolRes, nil, err
}
res := &msi.SearchFileContentResult{
GitGrepOutput: cmdRes.Stdout,
}
return &mcp.CallToolResult{
// Gemini:
// A message like: Found 10 matching lines for regex "function\\s+myFunction" in directory src:\nsrc/file1.js:10:function myFunction() {...}\nsrc/subdir/file2.ts:45: function myFunction(param) {...}...
StructuredContent: res,
}, res, nil
}
13 changes: 4 additions & 9 deletions pkg/mcp/toolset/shell.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ package toolset
import (
"bytes"
"context"
"encoding/json"
"errors"
"os/exec"

Expand All @@ -18,7 +17,7 @@ import (

func (ts *ToolSet) RunShellCommand(ctx context.Context,
_ *mcp.CallToolRequest, args msi.RunShellCommandParams,
) (*mcp.CallToolResult, any, error) {
) (*mcp.CallToolResult, *msi.RunShellCommandResult, error) {
if ts.inst == nil {
return nil, nil, errors.New("instance not registered")
}
Expand All @@ -33,7 +32,7 @@ func (ts *ToolSet) RunShellCommand(ctx context.Context,
cmd.Stdout = &stdout
cmd.Stderr = &stderr
cmdErr := cmd.Run()
res := msi.RunShellCommandResult{
res := &msi.RunShellCommandResult{
Stdout: stdout.String(),
Stderr: stderr.String(),
}
Expand All @@ -45,11 +44,7 @@ func (ts *ToolSet) RunShellCommand(ctx context.Context,
res.ExitCode = ptr.Of(st.ExitCode())
}
}
resJ, err := json.Marshal(res)
if err != nil {
return nil, nil, err
}
return &mcp.CallToolResult{
Content: []mcp.Content{&mcp.TextContent{Text: string(resJ)}},
}, nil, nil
StructuredContent: res,
}, res, nil
}