Skip to content

Conversation

AkihiroSuda
Copy link
Member

@AkihiroSuda AkihiroSuda commented Jul 17, 2025

This PR allows AI agents such as Gemini CLI to wrap local file operations (read/write/execute) inside Lima VM.

It should work with Claude Code, Codex, etc. too, but they might need a modification to disable their built-in local file operation tools. (Help wanted for testing)

This feature will be available in Lima v2.0

Preview of the documentation: https://deploy-preview-3744--lima-vm.netlify.app/docs/config/ai/


Interface

pkg/mcp/msi defines "MCP Sandbox Interface" (tentative) that should be reusable for other projects too.

MCP Sandbox Interface defines MCP (Model Context Protocol) tools that can be used for reading, writing, and executing local files with an appropriate sandboxing technology. The sandboxing technology can be more secure and/or efficient than the default tools provided by an AI agent.

MCP Sandbox Interface was inspired by Gemini CLI's built-in tools. https://github.com/google-gemini/gemini-cli/tree/v0.1.12/docs/tools

Implementation

limactl mcp serve INSTANCE launches an MCP server that implements the MCP Sandbox Interface.

Use https://github.com/modelcontextprotocol/inspector to play around with the server.

limactl start default
brew install mcp-inspector
mcp-inspector

In the web browser,

  • Set Command to limactl
  • Set Arguments to mcp serve default
  • Click ▶️Connect

Usage with Gemni CLI

  1. Create .gemini/extensions/lima/gemini-extension.json as follows:
{
  "name": "lima",
  "version": "2.0.0",
  "mcpServers": {
    "lima": {
      "command": "limactl",
      "args": [
        "mcp",
        "serve",
        "default"
      ]
    }
  }
}
  1. Modify .gemini/settings.json so as to disable Gemini CLI's built-in tools except ones that do not relate to local command execution and file I/O:
{
  "coreTools": ["WebFetchTool", "WebSearchTool", "MemoryTool"]
}

TODOs

  • Support writing files
  • Test

// - [RunShellCommandParams].Directory must not be empty
//
// Eventually, this package may be split to a separate repository.
package msi
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RFC: "MCP Sandbox Interface" might be a misnomer; it might rather sound like an interface for sandboxing MCP servers, e.g., docker run -i --rm example.com/some-mcp-server /usr/bin/some-mcp-server
https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md#docker-based-mcp-server

Alternative names:

  • AI Sandbox Interface
  • Agent Sandbox Interface
  • AI Protection Interface
  • ...

Thoughts?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @lima-vm/maintainers

@AkihiroSuda AkihiroSuda force-pushed the mcp branch 3 times, most recently from dadfb7c to ab6c5dc Compare July 17, 2025 15:36
@AkihiroSuda AkihiroSuda force-pushed the mcp branch 2 times, most recently from 0892d60 to 2967ffe Compare September 2, 2025 05:34
@jandubois
Copy link
Member

I've only browsed the code, but if I understand this correctly, the "protection" relies just on the fact that the VM has mounted the filesystem readonly. AFAICT there is no sandboxing of the filesystem, so the ReadFile tool could still read e.g. credentials from ~/.aws/config on the host and is not restricted to subdirectories of the current directory. And since you run arbitrary commands, this would need some kind of chroot jail, and not a pre-filtering of the arguments.

Beyond that, I wonder if this isn't outside the scope of Lima itself. It is an application built on top of Lima, but for some reason compiled into it. It could just as well be built as a separate tool. Or as a limactl-mcp plugin.

So I wonder if it wouldn't be good to include it as an example on how to create plugins for Lima? The reason to keep it in the Lima repo would be that MCP is a hot topic, and having it bundled makes it easier to discover.

@AkihiroSuda
Copy link
Member Author

I've only browsed the code, but if I understand this correctly, the "protection" relies just on the fact that the VM has mounted the filesystem readonly. AFAICT there is no sandboxing of the filesystem, so the ReadFile tool could still read e.g. credentials from ~/.aws/config on the host and is not restricted to subdirectories of the current directory. And since you run arbitrary commands, this would need some kind of chroot jail, and not a pre-filtering of the arguments.

The home directory except the pwd can be hidden with:

limactl start --mount-only "$(pwd)"

Beyond that, I wonder if this isn't outside the scope of Lima itself. It is an application built on top of Lima, but for some reason compiled into it. It could just as well be built as a separate tool. Or as a limactl-mcp plugin.

Maybe lima-mcp (or lima-mcpserver) so as to keep limac[TAB] shell completion working ?

@jandubois
Copy link
Member

jandubois commented Sep 3, 2025

Maybe lima-mcp (or lima-mcpserver) so as to keep limac[TAB] shell completion working ?

limactl-mcp would be the naming scheme we have already implemented. Tab completion still works, but you have to type the space yourself:

$ cat ~/bin/limactl-hello
#!/bin/bash
echo "Hello world from $BASH_SOURCE"

$ limactl hello
Hello world from /Users/jan/bin/limactl-hello

We could also look for plugins in /usr/local/share/libexec/lima to have an option to install them outside the PATH by default (we already look for external drivers in that directory).

@AkihiroSuda
Copy link
Member Author

Reimplemented as limactl-mcp CLI plugin

@AkihiroSuda AkihiroSuda force-pushed the mcp branch 2 times, most recently from 42eb5c5 to 206003b Compare September 9, 2025 08:21
@AkihiroSuda AkihiroSuda marked this pull request as ready for review September 9, 2025 08:21
@jandubois
Copy link
Member

It doesn't seem to be working for me:

l mcp -v
limactl-mcp version 2.0.0-alpha.0-39-gf71031e6l ls default qemu
NAME       STATUS     SSH                VMTYPE    ARCH       CPUS    MEMORY    DISK      DIR
default    Stopped    127.0.0.1:0        vz        aarch64    4       4GiB      100GiB    ~/.lima/default
qemu       Running    127.0.0.1:63754    qemu      aarch64    4       4GiB      100GiB    ~/.lima/qemul mcp serve default
FATA[0000] expected status of instance "default" to be "Running", got ""l mcp serve qemu
FATA[0000] expected status of instance "qemu" to be "Running", got ""l shell qemu uname -a
Linux lima-qemu 6.14.0-23-generic #23-Ubuntu SMP PREEMPT_DYNAMIC Fri Jun 13 22:12:54 UTC 2025 aarch64 aarch64 aarch64 GNU/Linux

@AkihiroSuda
Copy link
Member Author

It doesn't seem to be working for me:

Works for me.
Maybe you have some "non-standard" config? (e.g., custom LIMA_HOME)

@jandubois
Copy link
Member

Maybe you have some "non-standard" config? (e.g., custom LIMA_HOME)

No, I have the default setup. My suspicion is that store.Inspect does not work properly because limactl-mcp doesn't have the builtin drivers, and I don't have the drivers installed as external drivers. So loading of the driver-specific fields does not work.

I would have expected to see something like vmType "vz" is not registered or something like that, so don't know if the theory is correct.

Are you sure you don't have external qemu and vz drivers installed on your system, that might be responsible for making it work on your machine.

I think as a plugin the code has to call limactl ls INSTANCE --json as a subprocess instead of using store.Inspect.

@jandubois
Copy link
Member

Confirmed: The error goes away after running

ADDITIONAL_DRIVERS="qemu vz" make native additional-drivers limactl-plugins install

@jandubois
Copy link
Member

I would have expected to see something like vmType "vz" is not registered or something like that,

And it turns out to be #2668 once again wasting time. We really should fix this non-idiomatic interface.

When I add

@@ -151,6 +152,9 @@ func mcpServeAction(cmd *cobra.Command, args []string) error {
 	if err != nil {
 		return err
 	}
+	if len(inst.Errors) != 0 {
+		return errors.Join(inst.Errors...)
+	}
 	if err = ts.RegisterInstance(ctx, inst); err != nil {
 		return err
 	}

then I get a more meaningful error:

l mcp serve default
FATA[0000] failed to resolve vm for "/Users/jan/.lima/default/lima.yaml": vmType "vz" is not a registered driver

@jandubois
Copy link
Member

Is it a bug that make uninstall does not remove external drivers?

@unsuman
Copy link
Contributor

unsuman commented Sep 15, 2025

Is it a bug that make uninstall does not remove external drivers?

I guess for a dev env, make clean should remove the external drivers. Not sure how make uninstall works though?

@jandubois
Copy link
Member

I guess for a dev env, make clean should remove the external drivers. Not sure how make uninstall works though?

You can just look it up in the Makefile. 😄

make clean just deletes _output with everything in it.

The install target actually runs uninstall first, to make sure it installs a consistent set of files. It tries do delete anything that has potentially been installed by make install before. I think we just forgot to add the external drivers to the uninstall target.

@unsuman
Copy link
Contributor

unsuman commented Sep 15, 2025

Oh, got it, thanks! I've raised a fix for the same #4034

@AkihiroSuda AkihiroSuda reopened this Sep 16, 2025
@AkihiroSuda AkihiroSuda marked this pull request as draft September 17, 2025 03:33
@AkihiroSuda AkihiroSuda marked this pull request as ready for review September 18, 2025 04:18
@AkihiroSuda
Copy link
Member Author

Fixed the incompatibility with built-in drivers

=== Interface ===

`pkg/mcp/msi` defines "MCP Sandbox Interface" (tentative)
that should be reusable for other projects too.

MCP Sandbox Interface defines MCP (Model Context Protocol) tools
that can be used for reading, writing, and executing local files
with an appropriate sandboxing technology. The sandboxing technology
can be more secure and/or efficient than the default tools provided
by an AI agent.

MCP Sandbox Interface was inspired by Gemini CLI's built-in tools.
https://github.com/google-gemini/gemini-cli/tree/v0.1.12/docs/tools

=== Implementation ===

`limactl mcp serve INSTANCE` launches an MCP server that implements the MCP
Sandbox Interface.

Use <https://github.com/modelcontextprotocol/inspector>
to play around with the server.

```bash
limactl start default
brew install mcp-inspector
mcp-inspector
```
In the web browser,
- Set `Command` to `limactl`
- Set `Arguments` to `mcp serve default`
- Click `▶️Connect`

=== Usage with Gemni CLI ===

1. Create `.gemini/extensions/lima/gemini-extension.json` as follows:
```json
{
  "name": "lima",
  "version": "2.0.0",
  "mcpServers": {
    "lima": {
      "command": "limactl",
      "args": [
        "mcp",
        "serve",
        "default"
      ]
    }
  }
}
```

2. Modify `.gemini/settings.json` so as to disable Gemini CLI's built-in tools
except ones that do not relate to local command execution and file I/O:
```json
{
  "coreTools": ["WebFetchTool", "WebSearchTool", "MemoryTool"]
}
```

Signed-off-by: Akihiro Suda <[email protected]>

Drop the `:w` suffix if you do not want to allow writing to the mounted directory.

1. Create `.gemini/extensions/lima/gemini-extension.json` as follows:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
1. Create `.gemini/extensions/lima/gemini-extension.json` as follows:
2. Create `.gemini/extensions/lima/gemini-extension.json` as follows:

}
```

1. Modify `.gemini/settings.json` so as to disable Gemini CLI's [built-in tools](https://github.com/google-gemini/gemini-cli/tree/main/docs/tools)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
1. Modify `.gemini/settings.json` so as to disable Gemini CLI's [built-in tools](https://github.com/google-gemini/gemini-cli/tree/main/docs/tools)
3. Modify `.gemini/settings.json` so as to disable Gemini CLI's [built-in tools](https://github.com/google-gemini/gemini-cli/tree/main/docs/tools)

---

| ⚡ Requirement | Lima >= 2.0 |
|-------------------|-------------|
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
|-------------------|-------------|
|------------------|-------------|

serverOpts.Instructions += fmt.Sprintf(`

NOTE: the guest OS of the VM is Linux, while the host OS is %s.
`, strings.ToTitle(runtime.GOOS))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

strings.ToTitle(runtime.GOOS)) capitalizes all chars:

  • darwin -> DARWIN
  • windows -> WINDOWS

Do you think it is intentional?

Comment on lines +19 to +22
limactl := os.Getenv("LIMACTL")
if limactl == "" {
limactl = "limactl"
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be slightly shorter:

Suggested change
limactl := os.Getenv("LIMACTL")
if limactl == "" {
limactl = "limactl"
}
limactl := cmp.Or(os.Getenv("LIMACTL"), "limactl")


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'.`,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Description: `searches for a regular expression pattern within the content of files in a specified directory. Internally calls 'git grep -n --no-index'.`,
Description: `Searches for a regular expression pattern within the content of files in a specified directory. Internally calls 'git grep -n --no-index'.`,

Comment on lines +46 to +51
ssh := append([]string{sshExe.Exe}, sshExe.Args...)
ssh = append(ssh, "-F",
filepath.Join(inst.Dir, filenames.SSHConfig),
hostname.FromInstName(inst.Name))

cmd := exec.CommandContext(ctx, ssh[0], append(ssh[1:], "-s", "sftp")...)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it's simpler with slices.Concat:

Suggested change
ssh := append([]string{sshExe.Exe}, sshExe.Args...)
ssh = append(ssh, "-F",
filepath.Join(inst.Dir, filenames.SSHConfig),
hostname.FromInstName(inst.Name))
cmd := exec.CommandContext(ctx, ssh[0], append(ssh[1:], "-s", "sftp")...)
args := slices.Concat(sshExe.Args, []string{"-F", filepath.Join(inst.Dir, filenames.SSHConfig), hostname.FromInstName(inst.Name), "-s", "sftp"})
cmd := exec.CommandContext(ctx, sshExe.Exe, args...)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants