Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
lima-vm
GitHub Repository: lima-vm/lima
Path: blob/master/pkg/mcp/toolset/toolset.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
"fmt"
10
"os"
11
"os/exec"
12
"path/filepath"
13
"slices"
14
15
"github.com/modelcontextprotocol/go-sdk/mcp"
16
"github.com/pkg/sftp"
17
"github.com/sirupsen/logrus"
18
19
"github.com/lima-vm/lima/v2/pkg/instance/hostname"
20
"github.com/lima-vm/lima/v2/pkg/limatype"
21
"github.com/lima-vm/lima/v2/pkg/limatype/filenames"
22
"github.com/lima-vm/lima/v2/pkg/mcp/msi"
23
"github.com/lima-vm/lima/v2/pkg/sshutil"
24
)
25
26
func New(limactl string) (*ToolSet, error) {
27
ts := &ToolSet{
28
limactl: limactl,
29
}
30
return ts, nil
31
}
32
33
type ToolSet struct {
34
limactl string // needed for `limactl shell --workdir`
35
36
// Set on RegisterInstance()
37
inst *limatype.Instance
38
sftp *sftp.Client
39
sftpCmd *exec.Cmd
40
}
41
42
func newSFTPClient(ctx context.Context, inst *limatype.Instance) (*sftp.Client, *exec.Cmd, error) {
43
sshExe, err := sshutil.NewSSHExe()
44
if err != nil {
45
return nil, nil, err
46
}
47
args := slices.Concat(sshExe.Args, []string{"-F", filepath.Join(inst.Dir, filenames.SSHConfig), hostname.FromInstName(inst.Name), "-s", "sftp"})
48
cmd := exec.CommandContext(ctx, sshExe.Exe, args...)
49
cmd.Stderr = os.Stderr
50
w, err := cmd.StdinPipe()
51
if err != nil {
52
return nil, nil, err
53
}
54
r, err := cmd.StdoutPipe()
55
if err != nil {
56
return nil, nil, err
57
}
58
if err = cmd.Start(); err != nil {
59
return nil, nil, err
60
}
61
client, err := sftp.NewClientPipe(r, w)
62
if err != nil {
63
if cmd != nil && cmd.Process != nil {
64
_ = cmd.Process.Kill()
65
}
66
}
67
return client, cmd, err
68
}
69
70
func (ts *ToolSet) RegisterInstance(ctx context.Context, inst *limatype.Instance) error {
71
if inst.Status != limatype.StatusRunning {
72
return fmt.Errorf("expected status of instance %q to be %q, got %q",
73
inst.Name, limatype.StatusRunning, inst.Status)
74
}
75
if len(inst.Config.Mounts) == 0 {
76
logrus.Warnf("instance %q has no mount", inst.Name)
77
}
78
ts.inst = inst
79
var err error
80
ts.sftp, ts.sftpCmd, err = newSFTPClient(ctx, inst)
81
return err
82
}
83
84
func (ts *ToolSet) RegisterServer(server *mcp.Server) error {
85
mcp.AddTool(server, msi.ListDirectory, ts.ListDirectory)
86
mcp.AddTool(server, msi.ReadFile, ts.ReadFile)
87
mcp.AddTool(server, msi.WriteFile, ts.WriteFile)
88
mcp.AddTool(server, msi.Glob, ts.Glob)
89
mcp.AddTool(server, msi.SearchFileContent, ts.SearchFileContent)
90
mcp.AddTool(server, msi.RunShellCommand, ts.RunShellCommand)
91
return nil
92
}
93
94
func (ts *ToolSet) Close() error {
95
var err error
96
if ts.sftp != nil {
97
err = errors.Join(err, ts.sftp.Close())
98
}
99
if ts.sftpCmd != nil && ts.sftpCmd.Process != nil {
100
err = errors.Join(err, ts.sftpCmd.Process.Kill())
101
}
102
return err
103
}
104
105
func (ts *ToolSet) TranslateHostPath(hostPath string) (string, error) {
106
if hostPath == "" {
107
return "", errors.New("path is empty")
108
}
109
if !filepath.IsAbs(hostPath) {
110
return "", fmt.Errorf("expected an absolute path, got a relative path: %q", hostPath)
111
}
112
// TODO: make sure that hostPath is mounted
113
return hostPath, nil
114
}
115
116