Skip to content

Commit f47b60e

Browse files
authored
Merge pull request #4119 from AkihiroSuda/fix-4070
mcp: use StructuredContent; fix "path is empty" error
2 parents 590297a + 9e735aa commit f47b60e

File tree

3 files changed

+54
-33
lines changed

3 files changed

+54
-33
lines changed

pkg/mcp/msi/filesystem.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ var ReadFile = &mcp.Tool{
4242
Description: `Reads and returns the content of a specified file.`,
4343
}
4444

45+
type ReadFileResult struct {
46+
Content string `json:"content" jsonschema:"The content of the file."`
47+
}
48+
4549
type ReadFileParams struct {
4650
Path string `json:"path" jsonschema:"The absolute path to the file to read."`
4751
// TODO: Offset *int `json:"offset,omitempty" jsonschema:"For text files, the 0-based line number to start reading from. Requires limit to be set."`
@@ -53,6 +57,10 @@ var WriteFile = &mcp.Tool{
5357
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.`,
5458
}
5559

60+
type WriteFileResult struct {
61+
// Empty for now
62+
}
63+
5664
type WriteFileParams struct {
5765
Path string `json:"path" jsonschema:"The absolute path to the file to write to."`
5866
Content string `json:"content" jsonschema:"The content to write into the file."`
@@ -69,6 +77,10 @@ type GlobParams struct {
6977
// TODO: CaseSensitive bool `json:"case_sensitive,omitempty" jsonschema:": Whether the search should be case-sensitive. Defaults to false."`
7078
}
7179

80+
type GlobResult struct {
81+
Matches []string `json:"matches" jsonschema:"A list of absolute file paths that match the provided glob pattern."`
82+
}
83+
7284
var SearchFileContent = &mcp.Tool{
7385
Name: "search_file_content",
7486
Description: `Searches for a regular expression pattern within the content of files in a specified directory. Internally calls 'git grep -n --no-index'.`,
@@ -80,4 +92,8 @@ type SearchFileContentParams struct {
8092
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)."`
8193
}
8294

95+
type SearchFileContentResult struct {
96+
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."`
97+
}
98+
8399
// TODO: implement Replace

pkg/mcp/toolset/filesystem.go

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ package toolset
55

66
import (
77
"context"
8-
"encoding/json"
98
"errors"
109
"io"
1110
"os"
@@ -19,7 +18,7 @@ import (
1918

2019
func (ts *ToolSet) ListDirectory(ctx context.Context,
2120
_ *mcp.CallToolRequest, args msi.ListDirectoryParams,
22-
) (*mcp.CallToolResult, any, error) {
21+
) (*mcp.CallToolResult, *msi.ListDirectoryResult, error) {
2322
if ts.inst == nil {
2423
return nil, nil, errors.New("instance not registered")
2524
}
@@ -31,7 +30,7 @@ func (ts *ToolSet) ListDirectory(ctx context.Context,
3130
if err != nil {
3231
return nil, nil, err
3332
}
34-
res := msi.ListDirectoryResult{
33+
res := &msi.ListDirectoryResult{
3534
Entries: make([]msi.ListDirectoryResultEntry, len(guestEnts)),
3635
}
3736
for i, f := range guestEnts {
@@ -41,18 +40,14 @@ func (ts *ToolSet) ListDirectory(ctx context.Context,
4140
res.Entries[i].ModTime = ptr.Of(f.ModTime())
4241
res.Entries[i].IsDir = ptr.Of(f.IsDir())
4342
}
44-
resJ, err := json.Marshal(res)
45-
if err != nil {
46-
return nil, nil, err
47-
}
4843
return &mcp.CallToolResult{
49-
Content: []mcp.Content{&mcp.TextContent{Text: string(resJ)}},
50-
}, nil, nil
44+
StructuredContent: res,
45+
}, res, nil
5146
}
5247

5348
func (ts *ToolSet) ReadFile(_ context.Context,
5449
_ *mcp.CallToolRequest, args msi.ReadFileParams,
55-
) (*mcp.CallToolResult, any, error) {
50+
) (*mcp.CallToolResult, *msi.ReadFileResult, error) {
5651
if ts.inst == nil {
5752
return nil, nil, errors.New("instance not registered")
5853
}
@@ -71,17 +66,20 @@ func (ts *ToolSet) ReadFile(_ context.Context,
7166
if err != nil {
7267
return nil, nil, err
7368
}
69+
res := &msi.ReadFileResult{
70+
Content: string(b),
71+
}
7472
return &mcp.CallToolResult{
7573
// Gemini:
7674
// For text files: The file content, potentially prefixed with a truncation message
7775
// (e.g., [File content truncated: showing lines 1-100 of 500 total lines...]\nActual file content...).
78-
Content: []mcp.Content{&mcp.TextContent{Text: string(b)}},
79-
}, nil, nil
76+
StructuredContent: res,
77+
}, res, nil
8078
}
8179

8280
func (ts *ToolSet) WriteFile(_ context.Context,
8381
_ *mcp.CallToolRequest, args msi.WriteFileParams,
84-
) (*mcp.CallToolResult, any, error) {
82+
) (*mcp.CallToolResult, *msi.WriteFileResult, error) {
8583
if ts.inst == nil {
8684
return nil, nil, errors.New("instance not registered")
8785
}
@@ -98,17 +96,18 @@ func (ts *ToolSet) WriteFile(_ context.Context,
9896
if err != nil {
9997
return nil, nil, err
10098
}
99+
res := &msi.WriteFileResult{}
101100
return &mcp.CallToolResult{
102101
// Gemini:
103102
// A success message, e.g., `Successfully overwrote file: /path/to/your/file.txt`
104103
// or `Successfully created and wrote to new file: /path/to/new/file.txt.`
105-
Content: []mcp.Content{&mcp.TextContent{Text: "OK"}},
106-
}, nil, nil
104+
StructuredContent: res,
105+
}, res, nil
107106
}
108107

109108
func (ts *ToolSet) Glob(_ context.Context,
110109
_ *mcp.CallToolRequest, args msi.GlobParams,
111-
) (*mcp.CallToolResult, any, error) {
110+
) (*mcp.CallToolResult, *msi.GlobResult, error) {
112111
if ts.inst == nil {
113112
return nil, nil, errors.New("instance not registered")
114113
}
@@ -128,20 +127,19 @@ func (ts *ToolSet) Glob(_ context.Context,
128127
if err != nil {
129128
return nil, nil, err
130129
}
131-
resJ, err := json.Marshal(matches)
132-
if err != nil {
133-
return nil, nil, err
130+
res := &msi.GlobResult{
131+
Matches: matches,
134132
}
135133
return &mcp.CallToolResult{
136134
// Gemini:
137135
// A message like: Found 5 file(s) matching "*.ts" within src, sorted by modification time (newest first):\nsrc/file1.ts\nsrc/subdir/file2.ts...
138-
Content: []mcp.Content{&mcp.TextContent{Text: string(resJ)}},
139-
}, nil, nil
136+
StructuredContent: res,
137+
}, res, nil
140138
}
141139

142140
func (ts *ToolSet) SearchFileContent(ctx context.Context,
143141
req *mcp.CallToolRequest, args msi.SearchFileContentParams,
144-
) (*mcp.CallToolResult, any, error) {
142+
) (*mcp.CallToolResult, *msi.SearchFileContentResult, error) {
145143
if ts.inst == nil {
146144
return nil, nil, errors.New("instance not registered")
147145
}
@@ -159,7 +157,19 @@ func (ts *ToolSet) SearchFileContent(ctx context.Context,
159157
if args.Include != nil && *args.Include != "" {
160158
guestPath = path.Join(guestPath, *args.Include)
161159
}
162-
return ts.RunShellCommand(ctx, req, msi.RunShellCommandParams{
163-
Command: []string{"git", "grep", "-n", "--no-index", args.Pattern, guestPath},
160+
cmdToolRes, cmdRes, err := ts.RunShellCommand(ctx, req, msi.RunShellCommandParams{
161+
Command: []string{"git", "grep", "-n", "--no-index", args.Pattern, guestPath},
162+
Directory: pathStr, // Directory must be always set
164163
})
164+
if err != nil {
165+
return cmdToolRes, nil, err
166+
}
167+
res := &msi.SearchFileContentResult{
168+
GitGrepOutput: cmdRes.Stdout,
169+
}
170+
return &mcp.CallToolResult{
171+
// Gemini:
172+
// 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) {...}...
173+
StructuredContent: res,
174+
}, res, nil
165175
}

pkg/mcp/toolset/shell.go

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ package toolset
66
import (
77
"bytes"
88
"context"
9-
"encoding/json"
109
"errors"
1110
"os/exec"
1211

@@ -18,7 +17,7 @@ import (
1817

1918
func (ts *ToolSet) RunShellCommand(ctx context.Context,
2019
_ *mcp.CallToolRequest, args msi.RunShellCommandParams,
21-
) (*mcp.CallToolResult, any, error) {
20+
) (*mcp.CallToolResult, *msi.RunShellCommandResult, error) {
2221
if ts.inst == nil {
2322
return nil, nil, errors.New("instance not registered")
2423
}
@@ -33,7 +32,7 @@ func (ts *ToolSet) RunShellCommand(ctx context.Context,
3332
cmd.Stdout = &stdout
3433
cmd.Stderr = &stderr
3534
cmdErr := cmd.Run()
36-
res := msi.RunShellCommandResult{
35+
res := &msi.RunShellCommandResult{
3736
Stdout: stdout.String(),
3837
Stderr: stderr.String(),
3938
}
@@ -45,11 +44,7 @@ func (ts *ToolSet) RunShellCommand(ctx context.Context,
4544
res.ExitCode = ptr.Of(st.ExitCode())
4645
}
4746
}
48-
resJ, err := json.Marshal(res)
49-
if err != nil {
50-
return nil, nil, err
51-
}
5247
return &mcp.CallToolResult{
53-
Content: []mcp.Content{&mcp.TextContent{Text: string(resJ)}},
54-
}, nil, nil
48+
StructuredContent: res,
49+
}, res, nil
5550
}

0 commit comments

Comments
 (0)