diff --git a/Makefile b/Makefile index eacb5472..673888e7 100644 --- a/Makefile +++ b/Makefile @@ -478,7 +478,7 @@ endif # Set limactl.md as explicit dependency. # It's uncertain how many docsy pages will be generated by `make`, # because `limactl` generates them without corresponding source code for the docsy pages. -docsy: website/_output/docsy/limactl.md +docsy: website/_output/docsy/limactl.md website/_output/docsy-mcp/mcp.md website/_output/docsy/limactl.md: _output/bin/limactl$(exe) @mkdir -p website/_output/docsy ifeq ($(native_compiling),true) @@ -486,6 +486,11 @@ ifeq ($(native_compiling),true) $< generate-doc --type docsy website/_output/docsy \ --output _output --prefix $(PREFIX) endif +website/_output/docsy-mcp/mcp.md: _output/libexec/lima/limactl-mcp$(exe) + @mkdir -p website/_output/docsy-mcp +ifeq ($(native_compiling),true) + $< generate-doc website/_output/docsy-mcp +endif ################################################################################ default-template.yaml: _output/bin/limactl$(exe) diff --git a/cmd/limactl-mcp/main.go b/cmd/limactl-mcp/main.go index 0f6c567f..0195646d 100644 --- a/cmd/limactl-mcp/main.go +++ b/cmd/limactl-mcp/main.go @@ -4,9 +4,12 @@ package main import ( + "context" "encoding/json" "errors" "fmt" + "os" + "path/filepath" "runtime" "strings" @@ -38,6 +41,7 @@ func newApp() *cobra.Command { cmd.AddCommand( newMcpInfoCommand(), newMcpServeCommand(), + newMcpGenDocCommand(), // TODO: `limactl-mcp configure gemini` ? ) return cmd @@ -77,41 +81,10 @@ func newMcpInfoCommand() *cobra.Command { func mcpInfoAction(cmd *cobra.Command, _ []string) error { ctx := cmd.Context() - limactl, err := limactlutil.Path() + info, err := inspectInfo(ctx) if err != nil { return err } - ts, err := toolset.New(limactl) - if err != nil { - return err - } - server := newServer() - if err = ts.RegisterServer(server); err != nil { - return err - } - serverTransport, clientTransport := mcp.NewInMemoryTransports() - serverSession, err := server.Connect(ctx, serverTransport, nil) - if err != nil { - return err - } - client := mcp.NewClient(&mcp.Implementation{Name: "client"}, nil) - clientSession, err := client.Connect(ctx, clientTransport, nil) - if err != nil { - return err - } - toolsResult, err := clientSession.ListTools(ctx, &mcp.ListToolsParams{}) - if err != nil { - return err - } - if err = clientSession.Close(); err != nil { - return err - } - if err = serverSession.Wait(); err != nil { - return err - } - info := &Info{ - Tools: toolsResult.Tools, - } j, err := json.MarshalIndent(info, "", " ") if err != nil { return err @@ -120,6 +93,41 @@ func mcpInfoAction(cmd *cobra.Command, _ []string) error { return err } +func inspectInfo(ctx context.Context) (*Info, error) { + ts, err := toolset.New("") + if err != nil { + return nil, err + } + server := newServer() + if err = ts.RegisterServer(server); err != nil { + return nil, err + } + serverTransport, clientTransport := mcp.NewInMemoryTransports() + serverSession, err := server.Connect(ctx, serverTransport, nil) + if err != nil { + return nil, err + } + client := mcp.NewClient(&mcp.Implementation{Name: "client"}, nil) + clientSession, err := client.Connect(ctx, clientTransport, nil) + if err != nil { + return nil, err + } + toolsResult, err := clientSession.ListTools(ctx, &mcp.ListToolsParams{}) + if err != nil { + return nil, err + } + if err = clientSession.Close(); err != nil { + return nil, err + } + if err = serverSession.Wait(); err != nil { + return nil, err + } + info := &Info{ + Tools: toolsResult.Tools, + } + return info, nil +} + type Info struct { Tools []*mcp.Tool `json:"tools"` } @@ -170,3 +178,76 @@ func mcpServeAction(cmd *cobra.Command, args []string) error { transport := &mcp.StdioTransport{} return server.Run(ctx, transport) } + +func newMcpGenDocCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "generate-doc DIR", + Short: "Generate documentation pages", + Args: cobra.MinimumNArgs(1), + RunE: mcpGenDocAction, + Hidden: true, + } + return cmd +} + +func mcpGenDocAction(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + dir := args[0] + if err := os.MkdirAll(dir, 0o755); err != nil { + return err + } + fName := filepath.Join(dir, "mcp.md") + f, err := os.Create(fName) + if err != nil { + return err + } + defer f.Close() + fmt.Fprint(f, `--- +title: MCP tools +weight: 99 +--- +Lima implements the "MCP Sandbox Interface" (tentative name): +https://pkg.go.dev/github.com/lima-vm/lima/v2/pkg/mcp/msi + +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, such as Lima. + +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 +[Google Gemini CLI's built-in tools](https://github.com/google-gemini/gemini-cli/tree/main/docs/tools). + +`) + info, err := inspectInfo(ctx) + if err != nil { + return err + } + for _, tool := range info.Tools { + fmt.Fprintf(f, "## `%s`\n\n", tool.Name) + if tool.Title != "" { + fmt.Fprintf(f, "### Title\n\n%s\n\n", tool.Title) + } + if tool.Description != "" { + fmt.Fprintf(f, "### Description\n\n%s\n\n", tool.Description) + } + if tool.InputSchema != nil { + fmt.Fprint(f, "### Input Schema\n\n") + schema, err := json.MarshalIndent(tool.InputSchema, "", " ") + if err != nil { + return err + } + fmt.Fprintf(f, "```json\n%s\n```\n\n", string(schema)) + } + if tool.OutputSchema != nil { + fmt.Fprint(f, "### Output Schema\n\n") + schema, err := json.MarshalIndent(tool.OutputSchema, "", " ") + if err != nil { + return err + } + fmt.Fprintf(f, "```json\n%s\n```\n\n", string(schema)) + } + } + return f.Close() +} diff --git a/website/hugo.toml b/website/hugo.toml index 41b10795..b0748201 100644 --- a/website/hugo.toml +++ b/website/hugo.toml @@ -204,6 +204,10 @@ enable = false source="docsy" target="content/docs/Reference" language="en" + [[module.imports.mounts]] + source="docsy-mcp" + target="content/docs/config/ai/outside" + language="en" # Import templates README.md from templates folder [[module.imports]] path="../.."