Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
lima-vm
GitHub Repository: lima-vm/lima
Path: blob/master/pkg/mcp/toolset/filesystem.go
2604 views
1
// SPDX-FileCopyrightText: Copyright The Lima Authors
2
// SPDX-License-Identifier: Apache-2.0
3
4
package toolset
5
6
import (
7
"context"
8
"errors"
9
"io"
10
"os"
11
"path"
12
"path/filepath"
13
14
"github.com/modelcontextprotocol/go-sdk/mcp"
15
16
"github.com/lima-vm/lima/v2/pkg/mcp/msi"
17
"github.com/lima-vm/lima/v2/pkg/ptr"
18
)
19
20
func (ts *ToolSet) ListDirectory(ctx context.Context,
21
_ *mcp.CallToolRequest, args msi.ListDirectoryParams,
22
) (*mcp.CallToolResult, *msi.ListDirectoryResult, error) {
23
if ts.inst == nil {
24
return nil, nil, errors.New("instance not registered")
25
}
26
guestPath, err := ts.TranslateHostPath(args.Path)
27
if err != nil {
28
return nil, nil, err
29
}
30
guestEnts, err := ts.sftp.ReadDirContext(ctx, guestPath)
31
if err != nil {
32
return nil, nil, err
33
}
34
res := &msi.ListDirectoryResult{
35
Entries: make([]msi.ListDirectoryResultEntry, len(guestEnts)),
36
}
37
for i, f := range guestEnts {
38
res.Entries[i].Name = f.Name()
39
res.Entries[i].Size = ptr.Of(f.Size())
40
res.Entries[i].Mode = ptr.Of(f.Mode())
41
res.Entries[i].ModTime = ptr.Of(f.ModTime())
42
res.Entries[i].IsDir = ptr.Of(f.IsDir())
43
}
44
return &mcp.CallToolResult{
45
StructuredContent: res,
46
}, res, nil
47
}
48
49
func (ts *ToolSet) ReadFile(_ context.Context,
50
_ *mcp.CallToolRequest, args msi.ReadFileParams,
51
) (*mcp.CallToolResult, *msi.ReadFileResult, error) {
52
if ts.inst == nil {
53
return nil, nil, errors.New("instance not registered")
54
}
55
guestPath, err := ts.TranslateHostPath(args.Path)
56
if err != nil {
57
return nil, nil, err
58
}
59
f, err := ts.sftp.Open(guestPath)
60
if err != nil {
61
return nil, nil, err
62
}
63
defer f.Close()
64
const limitBytes = 32 * 1024 * 1024
65
lr := io.LimitReader(f, limitBytes)
66
b, err := io.ReadAll(lr)
67
if err != nil {
68
return nil, nil, err
69
}
70
res := &msi.ReadFileResult{
71
Content: string(b),
72
}
73
return &mcp.CallToolResult{
74
// Gemini:
75
// For text files: The file content, potentially prefixed with a truncation message
76
// (e.g., [File content truncated: showing lines 1-100 of 500 total lines...]\nActual file content...).
77
StructuredContent: res,
78
}, res, nil
79
}
80
81
func (ts *ToolSet) WriteFile(_ context.Context,
82
_ *mcp.CallToolRequest, args msi.WriteFileParams,
83
) (*mcp.CallToolResult, *msi.WriteFileResult, error) {
84
if ts.inst == nil {
85
return nil, nil, errors.New("instance not registered")
86
}
87
guestPath, err := ts.TranslateHostPath(args.Path)
88
if err != nil {
89
return nil, nil, err
90
}
91
dir := filepath.Dir(guestPath)
92
err = ts.sftp.MkdirAll(dir)
93
if err != nil {
94
return nil, nil, err
95
}
96
f, err := ts.sftp.Create(guestPath)
97
if err != nil {
98
return nil, nil, err
99
}
100
defer f.Close()
101
_, err = f.Write([]byte(args.Content))
102
if err != nil {
103
return nil, nil, err
104
}
105
res := &msi.WriteFileResult{}
106
return &mcp.CallToolResult{
107
// Gemini:
108
// A success message, e.g., `Successfully overwrote file: /path/to/your/file.txt`
109
// or `Successfully created and wrote to new file: /path/to/new/file.txt.`
110
StructuredContent: res,
111
}, res, nil
112
}
113
114
func (ts *ToolSet) Glob(_ context.Context,
115
_ *mcp.CallToolRequest, args msi.GlobParams,
116
) (*mcp.CallToolResult, *msi.GlobResult, error) {
117
if ts.inst == nil {
118
return nil, nil, errors.New("instance not registered")
119
}
120
pathStr, err := os.Getwd()
121
if err != nil {
122
return nil, nil, err
123
}
124
if args.Path != nil && *args.Path != "" {
125
pathStr = *args.Path
126
}
127
guestPath, err := ts.TranslateHostPath(pathStr)
128
if err != nil {
129
return nil, nil, err
130
}
131
pattern := path.Join(guestPath, args.Pattern)
132
matches, err := ts.sftp.Glob(pattern)
133
if matches == nil {
134
matches = []string{}
135
}
136
if err != nil {
137
return nil, nil, err
138
}
139
res := &msi.GlobResult{
140
Matches: matches,
141
}
142
return &mcp.CallToolResult{
143
// Gemini:
144
// A message like: Found 5 file(s) matching "*.ts" within src, sorted by modification time (newest first):\nsrc/file1.ts\nsrc/subdir/file2.ts...
145
StructuredContent: res,
146
}, res, nil
147
}
148
149
func (ts *ToolSet) SearchFileContent(ctx context.Context,
150
req *mcp.CallToolRequest, args msi.SearchFileContentParams,
151
) (*mcp.CallToolResult, *msi.SearchFileContentResult, error) {
152
if ts.inst == nil {
153
return nil, nil, errors.New("instance not registered")
154
}
155
pathStr, err := os.Getwd()
156
if err != nil {
157
return nil, nil, err
158
}
159
if args.Path != nil && *args.Path != "" {
160
pathStr = *args.Path
161
}
162
guestPath, err := ts.TranslateHostPath(pathStr)
163
if err != nil {
164
return nil, nil, err
165
}
166
if args.Include != nil && *args.Include != "" {
167
guestPath = path.Join(guestPath, *args.Include)
168
}
169
cmdToolRes, cmdRes, err := ts.RunShellCommand(ctx, req, msi.RunShellCommandParams{
170
Command: []string{"git", "grep", "-n", "--no-index", args.Pattern, guestPath},
171
Directory: pathStr, // Directory must be always set
172
})
173
if err != nil {
174
return cmdToolRes, nil, err
175
}
176
res := &msi.SearchFileContentResult{
177
GitGrepOutput: cmdRes.Stdout,
178
}
179
return &mcp.CallToolResult{
180
// Gemini:
181
// 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) {...}...
182
StructuredContent: res,
183
}, res, nil
184
}
185
186