diff --git a/MCP.md b/MCP.md index 2a55920..e77cfc5 100644 --- a/MCP.md +++ b/MCP.md @@ -94,6 +94,16 @@ mcp_servers: ## External MCP Servers +## Built-in filesystem as an MCP server (stdio) + +GoDex can run its built-in filesystem implementation as a standalone MCP server over stdio: + +```bash +godex mcp serve filesystem --allowed-path /home/user/project --allowed-path /tmp +``` + +Details: [MCP_BUILTIN_FILESYSTEM_SERVER.md](MCP_BUILTIN_FILESYSTEM_SERVER.md) + ### Chrome MCP Controller Control Chrome tabs via a browser extension. Requires the [chrome-mcp](https://github.com/cheikh2shift/chrome-mcp) server and Chrome extension. diff --git a/MCP_BUILTIN_FILESYSTEM_SERVER.md b/MCP_BUILTIN_FILESYSTEM_SERVER.md new file mode 100644 index 0000000..5c099e8 --- /dev/null +++ b/MCP_BUILTIN_FILESYSTEM_SERVER.md @@ -0,0 +1,181 @@ +# Built-in filesystem MCP server (`godex mcp serve filesystem`) + +GoDex contains a built-in filesystem tool implementation. + +This page documents how to run that built-in filesystem as a **standalone MCP server over stdio**, so other AI agents/clients can connect to it. + +## Run + +```bash +godex mcp serve filesystem +``` + +## Permissions (roots + allowlist) + +This server supports common MCP permission workflows used by other agents: + +- **Client roots** (recommended): when the connected client supports MCP *roots*, GoDex will request the roots list and treat them as allowed paths. +- **Explicit allowlist**: pass `--allowed-path` (repeatable) to add allowed paths up front. + +Examples: + +```bash +# Explicit allowlist +godex mcp serve filesystem --allowed-path /home/user/project --allowed-path /tmp + +# Disable roots usage (only use explicit allowlist / cwd fallback) +godex mcp serve filesystem --use-roots=false --allowed-path /tmp +``` + +`--auto-confirm` (unsafe) will automatically expand allowed paths when a request hits a restricted path. + +## Configure another agent/client + +Any MCP client that can launch stdio servers can run: + +```text +command: godex +args: ["mcp", "serve", "filesystem", "--allowed-path", "/path/to/workspace"] +transport: stdio +``` + +## Tools + +The server exposes a subset of GoDex’s filesystem tools, including: + +- `read_file`, `write_file`, `list_directory` +- `create_directory`, `delete_file` +- `search_files`, `search_in_file`, `search_directory_text`, `search_file_text` +- `get_file_info`, `read_file_line_range` +- `replace_first_in_file`, `replace_line_range`, `insert_at_line` +- `list_allowed_paths` + +## Add to OpenCode + +### Via config file + +In your `opencode.json` (project) or `~/.config/opencode/opencode.json` (global): + +```jsonc +{ + "mcp": { + "godex-filesystem": { + "type": "local", + "command": ["godex", "mcp", "serve", "filesystem", "--allowed-path", "/path/to/workspace"], + "enabled": true + } + } +} +``` + +Then restart OpenCode. + +### Via CLI + +```bash +npx @anthropic/anthropic-cli mcp add +# Or if opencode has the same tool: +opencode mcp add +``` + +## Add to Claude Desktop + +Edit `claude_desktop_config.json` (location varies by OS): +- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` +- Linux: `~/.config/Claude/claude_desktop_config.json` +- Windows: `%APPDATA%\Claude\claude_desktop_config.json` + +```json +{ + "mcpServers": { + "godex-filesystem": { + "command": "godex", + "args": ["mcp", "serve", "filesystem", "--allowed-path", "/path/to/workspace"], + "env": {} + } + } +} +``` + +## Add to Claude Code + +```bash +claude mcp add godex-filesystem --transport stdio -- godex mcp serve filesystem --allowed-path /path/to/workspace +``` + +Or via config in `.mcp.json` (project) or `~/.claude.json` (global): + +```jsonc +{ + "mcp": { + "godex-filesystem": { + "command": ["godex", "mcp", "serve", "filesystem", "--allowed-path", "/path/to/workspace"], + "env": {} + } + } +} +``` + +## Add to Codex + +Run: + +```bash +codex mcp add godex-filesystem -- godex mcp serve filesystem --allowed-path /path/to/workspace +``` + +Or edit `~/.codex/config.toml`: + +```toml +[mcp_servers.godex-filesystem] +command = "godex" +args = ["mcp", "serve", "filesystem", "--allowed-path", "/path/to/workspace"] +``` + +For workspace-specific config, create `.codex/config.toml` in your project. + +## Add to Cursor + +Cursor doesn't have a CLI command for adding MCP servers. Edit `~/.cursor/mcp.json`: + +```json +{ + "mcpServers": { + "godex-filesystem": { + "command": "godex", + "args": ["mcp", "serve", "filesystem", "--allowed-path", "/path/to/workspace"] + } + } +} +``` + +Or add via Settings > Tools & MCP > Add > Enter custom server details. + +## Add to VS Code + +### Via Command Palette +1. Press `Ctrl+Shift+P` (or `Cmd+Shift+P` on Mac) +2. Search **MCP: Add Server** +3. Select **Local** (stdio) +4. Enter command: `godex mcp serve filesystem --allowed-path /path/to/workspace` + +### Via CLI + +```bash +code --add-mcp '{"name":"godex-filesystem","command":"godex","args":["mcp","serve","filesystem","--allowed-path","/path/to/workspace"]}' +``` + +### Via config file +Edit `.vscode/mcp.json` (workspace) or run **MCP: Open User Configuration** to edit global config: + +```json +{ + "servers": { + "godex-filesystem": { + "command": "godex", + "args": ["mcp", "serve", "filesystem", "--allowed-path", "/path/to/workspace"] + } + } +} +``` + diff --git a/README.md b/README.md index 49c61d8..1135b6d 100644 --- a/README.md +++ b/README.md @@ -222,6 +222,8 @@ GoDex includes built-in MCP servers: For detailed MCP configuration including external servers, see [MCP.md](MCP.md). +To expose the built-in filesystem MCP server to other agents/clients over stdio, see [MCP_BUILTIN_FILESYSTEM_SERVER.md](MCP_BUILTIN_FILESYSTEM_SERVER.md). + ### Hive Network GoDex supports a Hive network mode where multiple instances can delegate tasks to each other. See [HIVE.md](HIVE.md) for details. diff --git a/cmd/godex/completion.go b/cmd/godex/completion.go index 19ba5d4..22f85d5 100644 --- a/cmd/godex/completion.go +++ b/cmd/godex/completion.go @@ -583,7 +583,7 @@ complete -c godex -l debug -s d -f complete -c godex -l completion -x -a "bash zsh fish" -d "Generate shell completion" complete -c godex -l llama-server -s l -x -d "External llama.cpp server URL" complete -c godex -n "__fish_use_subcommand" -a "mcp" -d "Manage MCP servers" -complete -c godex -n "__fish_seen_subcommand_from mcp" -a "add remove" -d "MCP subcommand" +complete -c godex -n "__fish_seen_subcommand_from mcp" -a "add remove serve" -d "MCP subcommand" complete -c godex -n "__fish_seen_subcommand_from mcp; and __fish_seen_subcommand_from add" -l provider -x -a "(__godex_complete_providers)" complete -c godex -n "__fish_seen_subcommand_from mcp; and __fish_seen_subcommand_from add" -l name -x -a "filesystem bash webscraper" complete -c godex -n "__fish_seen_subcommand_from mcp; and __fish_seen_subcommand_from add" -l command -x @@ -594,8 +594,12 @@ complete -c godex -n "__fish_seen_subcommand_from mcp; and __fish_seen_subcomman complete -c godex -n "__fish_seen_subcommand_from mcp; and __fish_seen_subcommand_from add" -l allowed-url -x complete -c godex -n "__fish_seen_subcommand_from mcp; and __fish_seen_subcommand_from remove" -l provider -x -a "(__godex_complete_providers)" complete -c godex -n "__fish_seen_subcommand_from mcp; and __fish_seen_subcommand_from remove" -l name -x -a "(__godex_complete_mcp_servers)" +complete -c godex -n "__fish_seen_subcommand_from mcp; and __fish_seen_subcommand_from serve" -a "filesystem" -d "Serve built-in MCP server" +complete -c godex -n "__fish_seen_subcommand_from mcp; and __fish_seen_subcommand_from serve; and __fish_seen_subcommand_from filesystem" -l allowed-path -x -a "(__fish_complete_directories)" +complete -c godex -n "__fish_seen_subcommand_from mcp; and __fish_seen_subcommand_from serve; and __fish_seen_subcommand_from filesystem" -l auto-confirm -f +complete -c godex -n "__fish_seen_subcommand_from mcp; and __fish_seen_subcommand_from serve; and __fish_seen_subcommand_from filesystem" -l use-roots -x -a "true false" `, defaultConfigPath, defaultConfigPath, defaultConfigPath) -} + } func printCompletionHelp() { fmt.Print(`Shell completion for godex diff --git a/cmd/godex/main.go b/cmd/godex/main.go index c65288c..6c5ad29 100644 --- a/cmd/godex/main.go +++ b/cmd/godex/main.go @@ -1323,6 +1323,7 @@ Flags: MCP subcommands: godex mcp add --provider --name [options] godex mcp remove --provider --name + godex mcp serve filesystem [options] MCP add options: --name MCP server name (filesystem, bash, webscraper, or external name) @@ -1347,7 +1348,7 @@ func (s *stringList) Set(value string) error { func handleMCPSubcommand(args []string, configPath string) error { if len(args) == 0 { - return fmt.Errorf("usage: godex mcp [options]") + return fmt.Errorf("usage: godex mcp [options]") } isFilesystem := func(name string) bool { @@ -1444,8 +1445,10 @@ func handleMCPSubcommand(args []string, configPath string) error { if err := config.Save(configPath, cfg); err != nil { return err } - fmt.Printf("Added MCP server %q to provider %q\n", serverName, provider.Name) - return nil + fmt.Printf("Added MCP server %q to provider %q\n", serverName, provider.Name) + return nil + case "serve": + return handleMCPServeSubcommand(args[1:]) case "remove": fs := flag.NewFlagSet("mcp remove", flag.ContinueOnError) var providerName string diff --git a/cmd/godex/mcp_serve.go b/cmd/godex/mcp_serve.go new file mode 100644 index 0000000..09f7bcd --- /dev/null +++ b/cmd/godex/mcp_serve.go @@ -0,0 +1,228 @@ +package main + +import ( + "context" + "flag" + "fmt" + "log" + "os" + "strings" + + "github.com/cheikh2shift/godex/internal/mcp" + mcplib "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +func handleMCPServeSubcommand(args []string) error { + if len(args) == 0 { + return fmt.Errorf("usage: godex mcp serve filesystem [options]") + } + + switch strings.ToLower(strings.TrimSpace(args[0])) { + case "filesystem", "files": + fs := flag.NewFlagSet("mcp serve filesystem", flag.ContinueOnError) + var allowedPaths stringList + var autoConfirm bool + var useRoots bool + fs.Var(&allowedPaths, "allowed-path", "allowed path (repeatable); if omitted, falls back to client roots or cwd") + fs.BoolVar(&autoConfirm, "auto-confirm", false, "auto-confirm restricted paths (unsafe)") + fs.BoolVar(&useRoots, "use-roots", true, "use client-provided MCP roots as additional allowed paths") + if err := fs.Parse(args[1:]); err != nil { + return err + } + return serveFilesystemMCP([]string(allowedPaths), autoConfirm, useRoots) + default: + return fmt.Errorf("unknown server: %s (supported: filesystem)", args[0]) + } +} + +func serveFilesystemMCP(baseAllowedPaths []string, autoConfirm bool, useRoots bool) error { + s := server.NewMCPServer( + "godex-filesystem", + version, + server.WithToolCapabilities(true), + server.WithRoots(), + ) + + resolveAllowedPaths := func(ctx context.Context) []string { + paths := append([]string{}, baseAllowedPaths...) + if useRoots { + if res, err := s.RequestRoots(ctx, mcplib.ListRootsRequest{}); err == nil { + for _, r := range res.Roots { + if strings.TrimSpace(r.URI) == "" { + continue + } + // Roots are usually file:// URIs, but some clients may send plain paths. + p := strings.TrimPrefix(r.URI, "file://") + if strings.TrimSpace(p) == "" { + continue + } + paths = append(paths, p) + } + } + } + if len(paths) == 0 { + if wd, err := os.Getwd(); err == nil && strings.TrimSpace(wd) != "" { + paths = []string{wd} + } + } + return uniqueStrings(paths) + } + + addProxyTool := func(name string, description string, schema mcplib.ToolInputSchema) { + s.AddTool(mcplib.Tool{ + Name: name, + Description: description, + InputSchema: schema, + }, func(ctx context.Context, request mcplib.CallToolRequest) (*mcplib.CallToolResult, error) { + allowed := resolveAllowedPaths(ctx) + fs := mcp.NewFileSystemServer(allowed, autoConfirm) + + args, _ := request.Params.Arguments.(map[string]any) + argMap := make(map[string]interface{}, len(args)) + for k, v := range args { + argMap[k] = v + } + + out, err := fs.CallTool(ctx, name, argMap) + if err != nil { + return mcplib.NewToolResultError(err.Error()), nil + } + return mcplib.NewToolResultText(out), nil + }) + } + + // Permissions helpers (works well with other agents' root/permission prompts). + s.AddTool(mcplib.Tool{ + Name: "list_allowed_paths", + Description: "List currently allowed filesystem paths (union of server flags and client-provided roots).", + InputSchema: mcplib.ToolInputSchema{Type: "object", Properties: map[string]any{}}, + }, func(ctx context.Context, request mcplib.CallToolRequest) (*mcplib.CallToolResult, error) { + paths := resolveAllowedPaths(ctx) + return mcplib.NewToolResultText(strings.Join(paths, "\n")), nil + }) + + // Core filesystem tools (proxy to internal built-in implementation). + addProxyTool("read_file", "Read contents of a file.", mcplib.ToolInputSchema{ + Type: "object", + Properties: map[string]any{ + "path": map[string]any{"type": "string", "description": "Absolute path to the file."}, + }, + Required: []string{"path"}, + }) + addProxyTool("write_file", "Write content to a file.", mcplib.ToolInputSchema{ + Type: "object", + Properties: map[string]any{ + "path": map[string]any{"type": "string", "description": "Absolute path to the file."}, + "content": map[string]any{"type": "string", "description": "Content to write."}, + }, + Required: []string{"path", "content"}, + }) + addProxyTool("list_directory", "List files in a directory.", mcplib.ToolInputSchema{ + Type: "object", + Properties: map[string]any{ + "path": map[string]any{"type": "string", "description": "Absolute path to directory."}, + }, + Required: []string{"path"}, + }) + addProxyTool("create_directory", "Create a new directory.", mcplib.ToolInputSchema{ + Type: "object", + Properties: map[string]any{ + "path": map[string]any{"type": "string", "description": "Directory path to create."}, + }, + Required: []string{"path"}, + }) + addProxyTool("delete_file", "Delete a file or directory.", mcplib.ToolInputSchema{ + Type: "object", + Properties: map[string]any{ + "path": map[string]any{"type": "string", "description": "File or directory path to delete."}, + }, + Required: []string{"path"}, + }) + addProxyTool("search_files", "Search for files matching a glob pattern.", mcplib.ToolInputSchema{ + Type: "object", + Properties: map[string]any{ + "path": map[string]any{"type": "string", "description": "Directory to search."}, + "pattern": map[string]any{"type": "string", "description": "File pattern (e.g., *.go)."}, + }, + Required: []string{"path", "pattern"}, + }) + addProxyTool("get_file_info", "Get information about a file.", mcplib.ToolInputSchema{ + Type: "object", + Properties: map[string]any{ + "path": map[string]any{"type": "string", "description": "File path."}, + }, + Required: []string{"path"}, + }) + addProxyTool("read_file_line_range", "Read a specific range of lines from a file.", mcplib.ToolInputSchema{ + Type: "object", + Properties: map[string]any{ + "path": map[string]any{"type": "string", "description": "File path."}, + "start": map[string]any{"type": "number", "description": "Start line number (1-indexed)."}, + "end": map[string]any{"type": "number", "description": "End line number (inclusive)."}, + }, + Required: []string{"path", "start", "end"}, + }) + addProxyTool("replace_first_in_file", "Replace the first occurrence of a string in a file.", mcplib.ToolInputSchema{ + Type: "object", + Properties: map[string]any{ + "path": map[string]any{"type": "string", "description": "File path."}, + "find": map[string]any{"type": "string", "description": "Text to find (plain string)."}, + "replace": map[string]any{"type": "string", "description": "Replacement text."}, + }, + Required: []string{"path", "find", "replace"}, + }) + addProxyTool("replace_line_range", "Replace a range of lines in a file.", mcplib.ToolInputSchema{ + Type: "object", + Properties: map[string]any{ + "path": map[string]any{"type": "string", "description": "File path."}, + "start": map[string]any{"type": "number", "description": "Start line number (1-indexed)."}, + "end": map[string]any{"type": "number", "description": "End line number (inclusive)."}, + "new_content": map[string]any{"type": "string", "description": "New content to insert at the deleted location."}, + }, + Required: []string{"path", "start", "end", "new_content"}, + }) + addProxyTool("insert_at_line", "Insert content at a specific line number.", mcplib.ToolInputSchema{ + Type: "object", + Properties: map[string]any{ + "path": map[string]any{"type": "string", "description": "File path."}, + "line": map[string]any{"type": "number", "description": "Line number to insert at (1-indexed, 0 = append at end)."}, + "content": map[string]any{"type": "string", "description": "Content to insert."}, + }, + Required: []string{"path", "line", "content"}, + }) + addProxyTool("search_file_text", "Search for text content within files in a directory.", mcplib.ToolInputSchema{ + Type: "object", + Properties: map[string]any{ + "path": map[string]any{"type": "string", "description": "Directory to search."}, + "query": map[string]any{"type": "string", "description": "Text to search for."}, + "case_sensitive": map[string]any{"type": "boolean", "description": "Enable case-sensitive matching."}, + "use_regex": map[string]any{"type": "boolean", "description": "Treat query as a regular expression."}, + }, + Required: []string{"path", "query"}, + }) + addProxyTool("search_in_file", "Search for text within a specific file.", mcplib.ToolInputSchema{ + Type: "object", + Properties: map[string]any{ + "path": map[string]any{"type": "string", "description": "File path."}, + "query": map[string]any{"type": "string", "description": "Text to search for (ignored if pattern provided)."}, + "pattern": map[string]any{"type": "string", "description": "Regex pattern (takes precedence)."}, + "case_sensitive": map[string]any{"type": "boolean", "description": "Enable case-sensitive matching."}, + "use_regex": map[string]any{"type": "boolean", "description": "Treat query as a regular expression."}, + }, + Required: []string{"path"}, + }) + addProxyTool("search_directory_text", "Search for text content within files in a directory.", mcplib.ToolInputSchema{ + Type: "object", + Properties: map[string]any{ + "path": map[string]any{"type": "string", "description": "Directory to search."}, + "query": map[string]any{"type": "string", "description": "Text to search for (ignored if pattern is provided)."}, + "pattern": map[string]any{"type": "string", "description": "Regex pattern to search for (takes precedence over query)."}, + "case_sensitive": map[string]any{"type": "boolean", "description": "Enable case-sensitive matching."}, + }, + Required: []string{"path"}, + }) + + log.Printf("[MCP] Serving built-in filesystem MCP server over stdio (autoConfirm=%v, useRoots=%v, baseAllowedPaths=%v)", autoConfirm, useRoots, baseAllowedPaths) + return server.ServeStdio(s) +}