diff --git a/README.md b/README.md index 646e6fa..b3cd0cf 100644 --- a/README.md +++ b/README.md @@ -222,9 +222,21 @@ lea symbols auth -k interface # Filter by kind lea symbols -p internal # Filter by package ``` -### 3. Start the MCP Server +### 3. Universal MCP Install (Recommended) -Connect your favorite AI agent (Claude Code, Aider, Pi, etc.) directly to your codebase: +Register `lea` and `lynx` as MCP tools across all supported AI agents in one command: + +```bash +lea mcp install +``` + +This auto-detects installed tools (Claude Code, VS Code Cline/Roo Code/Codex CLI, OpenCode, Pi, Zed, Gemini CLI, OpenClaw, Aider, Antigravity, Kiro, KiloCode) and injects the `pizen-lea` and `pizen-lynx` MCP entries into their config files — JSON, YAML, or TOML as appropriate. + +Also generates `~/.config/pizen/instructions.md` with the dual-tool orchestration protocol. + +### 4. Start the MCP Server + +Connect your favorite AI agent directly to your codebase: ```bash lea mcp @@ -232,7 +244,7 @@ lea mcp Exposes MCP tools: `get_symbol_context`, `find_neighbors`, `trace_calls`, `trace_execution_path`, `find_architecture_violations`. -### 4. Export agent configurations +### 5. Export agent configurations Generate bootstrap rule pointers for AI agent ecosystems: @@ -245,7 +257,7 @@ lea export copilot # Creates .github/copilot-instructions.md lea export pi # Creates .pi/AGENTS.md ``` -### 5. Interactive Exploration +### 6. Interactive Exploration Launch the TUI for fuzzy symbol search and dependency browsing: @@ -253,7 +265,7 @@ Launch the TUI for fuzzy symbol search and dependency browsing: lea tui ``` -### 6. Analyze impact and context +### 7. Analyze impact and context ```bash # Blast radius analysis @@ -272,7 +284,7 @@ lea flow "func:internal/cli/commands:Execute" lea neighbors AuthService ``` -### 7. Watch for changes +### 8. Watch for changes Real-time incremental indexing without full re-index: @@ -290,6 +302,7 @@ lea watch . | `symbols` | Discover and list symbols in the registry | `lea symbols auth -k interface` | | `tui` | Open the interactive symbol explorer (Bubble Tea) | `lea tui` | | `mcp` | Start the Model Context Protocol server (stdio) | `lea mcp` | +| `mcp install` | One-command MCP setup for lea & lynx across 11 AI tools | `lea mcp install` | | `export` | Generate AI agent configuration bootstrap pointers | `lea export claude` | | `context` | Generate budget-aware context for a symbol | `lea context AuthService --budget 2000` | | `trace` | Follow the recursive call graph from a function | `lea trace "func:internal/cli:Execute"` | @@ -479,6 +492,8 @@ sequenceDiagram - **Missing symbols?** Confirm the target language parser is supported (Go, Python, Rust, TypeScript). Non-Go languages use Tree-sitter — file a GitHub issue for unsupported languages. - **Architecture checks fail?** Ensure your rules file (e.g., `arch.yaml`) is present and valid YAML. Check layer patterns match your directory structure. - **MCP not connecting?** Verify the MCP server is running (`lea mcp`) and your AI agent is configured to connect to it via stdio. +- **MCP install skips a tool?** That's expected — `lea mcp install` only injects entries into tools whose config directory already exists on your system, avoiding noise from uninstalled applications. +- **Missing agent on the install list?** Run `lea mcp install` — it currently supports 11 targets (Claude Code, VS Code Cline/Roo/Codex CLI, OpenCode, Pi, Zed, Gemini CLI, OpenClaw, Aider, Antigravity, Kiro, KiloCode). - **TUI shows no symbols?** Ensure `lea index .` completed successfully — the TUI reads from `.lea/graph.db`. - **Export file missing?** Check that you ran `lea export ` from the repository root. The tool creates the necessary subdirectories automatically. @@ -542,7 +557,7 @@ lea/ │ │ ├── flow.go # lea flow │ │ ├── impact.go # lea impact (blast radius) │ │ ├── index.go # lea index (+metadata generation) -│ │ ├── mcp.go # lea mcp +│ │ ├── mcp.go # lea mcp (server + install) │ │ ├── neighbors.go # lea neighbors │ │ ├── root.go # Root command + symbol resolution │ │ ├── symbols.go # lea symbols @@ -554,7 +569,9 @@ lea/ │ ├── graph/contracts/ # Graph node/edge type definitions │ │ ├── edge.go # 8 edge types │ │ └── node.go # 7 node types -│ ├── mcp/server.go # MCP server (5 tools) +│ ├── mcp/ +│ │ ├── install/install.go # MCP install (11 target writers) +│ │ └── server.go # MCP server (5 tools) │ ├── parser/ │ │ ├── contracts/parser.go # Parser interface │ │ ├── golang/parser.go # Go AST parser with deep resolution diff --git a/internal/cli/commands/mcp.go b/internal/cli/commands/mcp.go index 6148c45..fe8b338 100644 --- a/internal/cli/commands/mcp.go +++ b/internal/cli/commands/mcp.go @@ -1,6 +1,7 @@ package commands import ( + "os" "path/filepath" "github.com/PizenLabs/lea/internal/mcp" @@ -9,12 +10,42 @@ import ( "github.com/spf13/cobra" ) +// findLeaDir walks up from dir looking for a .lea directory. +func findLeaDir(dir string) (string, error) { + abs, err := filepath.Abs(dir) + if err != nil { + return "", err + } + for { + candidate := filepath.Join(abs, ".lea") + if info, err := os.Stat(candidate); err == nil && info.IsDir() { + return candidate, nil + } + parent := filepath.Dir(abs) + if parent == abs { + return "", os.ErrNotExist + } + abs = parent + } +} + var mcpCmd = &cobra.Command{ - Use: "mcp", + Use: "mcp [path]", Short: "Start the MCP server to expose lea to AI agents", Long: `The mcp command starts a Model Context Protocol server over stdio, allowing AI agents like Claude or Pi to query the structural graph.`, - RunE: func(_ *cobra.Command, _ []string) error { - dbPath := filepath.Join(".lea", "graph.db") + Args: cobra.MaximumNArgs(1), + RunE: func(_ *cobra.Command, args []string) error { + dir := "." + if len(args) > 0 { + dir = args[0] + } + + leaDir, err := findLeaDir(dir) + if err != nil { + return err + } + + dbPath := filepath.Join(leaDir, "graph.db") store, err := sqlite.NewStore(dbPath) if err != nil { return err diff --git a/internal/mcp/install/install.go b/internal/mcp/install/install.go index 37189f5..779b604 100644 --- a/internal/mcp/install/install.go +++ b/internal/mcp/install/install.go @@ -18,8 +18,9 @@ import ( // MCPEntry represents a single MCP tool entry in the JSON config schema. type MCPEntry struct { - Command string `json:"command" yaml:"cmd" toml:"command"` - Args []string `json:"args" yaml:"-" toml:"args"` + Command string `json:"command" yaml:"cmd" toml:"command"` + Args []string `json:"args" yaml:"-" toml:"args"` + Env map[string]string `json:"env,omitempty" yaml:"-" toml:"-"` } type target struct { @@ -33,15 +34,14 @@ func installTargets() []target { home := homeDir() configDir := filepath.Join(home, ".config") - // VS Code / OpenCode base path is platform-dependent vscodeBase := vscodeGlobalStorageDir(home) - opencodeBase := opencodeGlobalStorageDir(home) return []target{ {Name: "Claude Code", Path: filepath.Join(home, ".claude", ".mcp.json"), Format: "json"}, {Name: "VS Code (Cline/Roo Code/Codex CLI)", Path: filepath.Join(vscodeBase, "saoudrizwan.claude-dev", "settings", "mcp_settings.json"), Format: "json"}, - {Name: "OpenCode", Path: filepath.Join(opencodeBase, "saoudrizwan.claude-dev", "settings", "mcp_settings.json"), Format: "json"}, - {Name: "Pi Coding Agents", Path: filepath.Join(home, ".pi", "agent", "mcp_config.json"), Format: "json"}, + {Name: "OpenCode", Path: filepath.Join(configDir, "opencode", "opencode.json"), Format: "opencode"}, + {Name: "Pi Coding Agents", Path: filepath.Join(home, ".pi", "agent", "mcp.json"), Format: "json"}, + {Name: "PizenLabs Shared MCP", Path: filepath.Join(configDir, "mcp", "mcp.json"), Format: "json"}, {Name: "Zed IDE", Path: filepath.Join(home, ".zed", "settings.json"), Format: "json"}, {Name: "Gemini CLI", Path: filepath.Join(configDir, "gemini-cli", "mcp.json"), Format: "json"}, {Name: "OpenClaw", Path: filepath.Join(configDir, "openclaw", "mcp.json"), Format: "json"}, @@ -79,25 +79,6 @@ func vscodeGlobalStorageDir(home string) string { } } -// opencodeGlobalStorageDir returns the OpenCode globalStorage path for the current OS. -func opencodeGlobalStorageDir(home string) string { - // OpenCode uses the same directory structure as VS Code under its own config root. - switch runtime.GOOS { - case "darwin": - return filepath.Join(home, "Library", "Application Support", "OpenCode", "User", "globalStorage") - case "linux": - return filepath.Join(home, ".config", "OpenCode", "User", "globalStorage") - case "windows": - appData := os.Getenv("APPDATA") - if appData == "" { - appData = filepath.Join(home, "AppData", "Roaming") - } - return filepath.Join(appData, "OpenCode", "User", "globalStorage") - default: - return filepath.Join(home, ".config", "OpenCode", "User", "globalStorage") - } -} - // Run configures all MCP targets with pizen-lea and pizen-lynx entries. func Run() error { // Resolve lea binary path @@ -154,12 +135,21 @@ func resolveLXFallback() string { func configureTarget(t target, leaPath, lxPath string) error { parent := filepath.Dir(t.Path) if _, err := os.Stat(parent); os.IsNotExist(err) { - return fmt.Errorf("parent directory %q does not exist", parent) + // Create parent directory for PizenLabs Shared MCP and other writable targets + if t.Name == "PizenLabs Shared MCP" { + if err := os.MkdirAll(parent, 0755); err != nil { + return fmt.Errorf("cannot create parent directory %q: %w", parent, err) + } + } else { + return fmt.Errorf("parent directory %q does not exist", parent) + } } switch t.Format { case "json": return injectJSON(t.Path, leaPath, lxPath) + case "opencode": + return injectOpenCodeJSON(t.Path, leaPath, lxPath) case "yaml": return injectYAML(t.Path, leaPath, lxPath) case "toml": @@ -192,8 +182,14 @@ func injectJSON(path, leaPath, lxPath string) error { if !ok || servers == nil { servers = make(map[string]any) } - servers["pizen-lea"] = MCPEntry{Command: leaPath, Args: []string{"mcp"}} - servers["pizen-lynx"] = MCPEntry{Command: lxPath, Args: []string{"mcp"}} + env := map[string]string{ + "PATH": os.Getenv("PATH"), + "HOME": os.Getenv("HOME"), + } + leaEntry := MCPEntry{Command: leaPath, Args: []string{"mcp"}, Env: env} + lxEntry := MCPEntry{Command: lxPath, Args: []string{"mcp"}, Env: env} + servers["pizen-lea"] = leaEntry + servers["pizen-lynx"] = lxEntry raw["mcpServers"] = servers return writeJSON(path, raw) @@ -206,9 +202,48 @@ func injectZedJSON(raw map[string]any, path, leaPath, lxPath string) error { mcp = make(map[string]any) } + env := map[string]string{ + "PATH": os.Getenv("PATH"), + "HOME": os.Getenv("HOME"), + } + // Zed format: "pizen-lea": { "command": "...", "args": ["mcp"] } - mcp["pizen-lea"] = MCPEntry{Command: leaPath, Args: []string{"mcp"}} - mcp["pizen-lynx"] = MCPEntry{Command: lxPath, Args: []string{"mcp"}} + mcp["pizen-lea"] = MCPEntry{Command: leaPath, Args: []string{"mcp"}, Env: env} + mcp["pizen-lynx"] = MCPEntry{Command: lxPath, Args: []string{"mcp"}, Env: env} + raw["mcp"] = mcp + + return writeJSON(path, raw) +} + +// injectOpenCodeJSON handles OpenCode's MCP config format under root "mcp" key. +// OpenCode uses: { "enabled": true, "type": "local", "command": ["path", "mcp"] } +func injectOpenCodeJSON(path, leaPath, lxPath string) error { + data := readOrEmpty(path) + + var raw map[string]any + if len(data) > 0 { + if err := json.Unmarshal(data, &raw); err != nil { + return fmt.Errorf("unmarshal error: %w", err) + } + } else { + raw = make(map[string]any) + } + + mcp, ok := raw["mcp"].(map[string]any) + if !ok || mcp == nil { + mcp = make(map[string]any) + } + + mcp["pizen-lea"] = map[string]any{ + "enabled": true, + "type": "local", + "command": []any{leaPath, "mcp"}, + } + mcp["pizen-lynx"] = map[string]any{ + "enabled": true, + "type": "local", + "command": []any{lxPath, "mcp"}, + } raw["mcp"] = mcp return writeJSON(path, raw) diff --git a/internal/mcp/server.go b/internal/mcp/server.go index dc58c90..91159bc 100644 --- a/internal/mcp/server.go +++ b/internal/mcp/server.go @@ -77,12 +77,18 @@ func (s *Server) Start() error { mcp_golang.WithName("lea"), ) - // Register all tools. if err := s.registerTools(server); err != nil { return err } - return server.Serve() + if err := server.Serve(); err != nil { + return err + } + + // mcp-golang v0.16.1 Serve() is non-blocking — the read-loop goroutine + // has already started. Block forever so the process stays alive until + // opencode kills the subprocess. + select {} } // registerTools registers every MCP tool exposed by lea.