mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-10 13:11:31 -10:00
* feat(cli): wire MCP serve WorkspaceRoot and --add-dir scope Pass the resolved workspace into mcp.ServeOptions and honor --add-dir so resources/list and scoped tools can expose extra roots beyond cwd. * fix(cli): keep lexical serve --add-dir roots for path matching Use symlink-resolved paths only for deduplication so Scope roots stay aligned with the un-evaluated WorkspaceRoot MCP serve already passes.
194 lines
5.7 KiB
Go
194 lines
5.7 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/Gitlawb/zero/internal/mcp"
|
|
"github.com/Gitlawb/zero/internal/redaction"
|
|
"github.com/Gitlawb/zero/internal/tools"
|
|
)
|
|
|
|
type serveOptions struct {
|
|
mcp bool
|
|
cwd string
|
|
addDirs []string
|
|
allowUnsafeTools bool
|
|
}
|
|
|
|
// serveRootsScope is a PathScope used only for MCP serve: the workspace root
|
|
// plus optional --add-dir roots, without the sandbox temp write roots that
|
|
// would otherwise pollute resources/list.
|
|
type serveRootsScope []string
|
|
|
|
func (s serveRootsScope) Roots() []string {
|
|
roots := make([]string, len(s))
|
|
copy(roots, s)
|
|
return roots
|
|
}
|
|
|
|
func runServe(args []string, stdout io.Writer, stderr io.Writer, deps appDeps) int {
|
|
options, help, err := parseServeArgs(args)
|
|
if err != nil {
|
|
return writeExecUsageError(stderr, err.Error())
|
|
}
|
|
if help {
|
|
if err := writeServeHelp(stdout); err != nil {
|
|
return exitCrash
|
|
}
|
|
return exitSuccess
|
|
}
|
|
if !options.mcp {
|
|
return writeExecUsageError(stderr, "serve requires --mcp. Use `zero serve --mcp`.")
|
|
}
|
|
|
|
workspaceRoot, err := resolveWorkspaceRoot(options.cwd, deps)
|
|
if err != nil {
|
|
return writeExecUsageError(stderr, err.Error())
|
|
}
|
|
scope, err := buildServeScope(workspaceRoot, options.addDirs)
|
|
if err != nil {
|
|
return writeExecUsageError(stderr, err.Error())
|
|
}
|
|
registry := newServeRegistry(workspaceRoot, scope, options.allowUnsafeTools)
|
|
if options.allowUnsafeTools {
|
|
if _, err := fmt.Fprintln(stderr, "[zero] Unsafe MCP server tools enabled because --allow-unsafe-tools was passed."); err != nil {
|
|
return exitCrash
|
|
}
|
|
}
|
|
|
|
err = mcp.Serve(context.Background(), deps.stdin, stdout, registry, mcp.ServeOptions{
|
|
Name: "zero",
|
|
Version: version,
|
|
PermissionGranted: options.allowUnsafeTools,
|
|
WorkspaceRoot: workspaceRoot,
|
|
Scope: scope,
|
|
})
|
|
if err != nil {
|
|
return writeAppError(stderr, redaction.ErrorMessage(err, redaction.Options{}), exitCrash)
|
|
}
|
|
return exitSuccess
|
|
}
|
|
|
|
func newServeRegistry(workspaceRoot string, scope tools.PathScope, allowUnsafeTools bool) *tools.Registry {
|
|
registry := tools.NewRegistry()
|
|
toolset := tools.CoreReadOnlyToolsScoped(workspaceRoot, scope)
|
|
if allowUnsafeTools {
|
|
toolset = tools.CoreToolsScoped(workspaceRoot, scope)
|
|
}
|
|
for _, tool := range toolset {
|
|
registry.Register(tool)
|
|
}
|
|
return registry
|
|
}
|
|
|
|
// buildServeScope returns nil when there are no extra roots (workspace-only),
|
|
// otherwise a PathScope listing the workspace first followed by each --add-dir.
|
|
func buildServeScope(workspaceRoot string, addDirs []string) (tools.PathScope, error) {
|
|
if len(addDirs) == 0 {
|
|
return nil, nil
|
|
}
|
|
roots := make([]string, 0, 1+len(addDirs))
|
|
workspaceRoot = filepath.Clean(workspaceRoot)
|
|
if abs, err := filepath.Abs(workspaceRoot); err == nil {
|
|
workspaceRoot = abs
|
|
}
|
|
|
|
// Keep lexical absolute paths in roots so path checks stay aligned with the
|
|
// un-evaluated WorkspaceRoot used when Scope is nil. Symlink-resolved forms
|
|
// are only keys for duplicate detection.
|
|
workspaceResolved := workspaceRoot
|
|
if resolved, err := filepath.EvalSymlinks(workspaceRoot); err == nil {
|
|
workspaceResolved = resolved
|
|
}
|
|
roots = append(roots, workspaceRoot)
|
|
|
|
seen := map[string]struct{}{workspaceResolved: {}}
|
|
for _, dir := range addDirs {
|
|
trimmed := strings.TrimSpace(dir)
|
|
if trimmed == "" {
|
|
return nil, execUsageError{"--add-dir requires a path"}
|
|
}
|
|
absolute, err := filepath.Abs(trimmed)
|
|
if err != nil {
|
|
return nil, execUsageError{fmt.Sprintf("--add-dir %q: %v", trimmed, err)}
|
|
}
|
|
info, err := os.Stat(absolute)
|
|
if err != nil {
|
|
return nil, execUsageError{fmt.Sprintf("--add-dir %q: %v", trimmed, err)}
|
|
}
|
|
if !info.IsDir() {
|
|
return nil, execUsageError{fmt.Sprintf("--add-dir %q is not a directory", trimmed)}
|
|
}
|
|
|
|
resolved := absolute
|
|
if r, err := filepath.EvalSymlinks(absolute); err == nil {
|
|
resolved = r
|
|
}
|
|
if _, ok := seen[resolved]; ok {
|
|
continue
|
|
}
|
|
seen[resolved] = struct{}{}
|
|
roots = append(roots, absolute)
|
|
}
|
|
return serveRootsScope(roots), nil
|
|
}
|
|
|
|
func parseServeArgs(args []string) (serveOptions, bool, error) {
|
|
options := serveOptions{}
|
|
for index := 0; index < len(args); index++ {
|
|
arg := args[index]
|
|
switch {
|
|
case arg == "-h" || arg == "--help" || arg == "help":
|
|
return options, true, nil
|
|
case arg == "--mcp":
|
|
options.mcp = true
|
|
case arg == "--allow-unsafe-tools":
|
|
options.allowUnsafeTools = true
|
|
case arg == "-C" || arg == "--cwd":
|
|
index++
|
|
if index >= len(args) {
|
|
return options, false, execUsageError{arg + " requires a path"}
|
|
}
|
|
options.cwd = args[index]
|
|
case strings.HasPrefix(arg, "--cwd="):
|
|
options.cwd = strings.TrimPrefix(arg, "--cwd=")
|
|
case arg == "--add-dir":
|
|
index++
|
|
if index >= len(args) {
|
|
return options, false, execUsageError{"--add-dir requires a path"}
|
|
}
|
|
options.addDirs = append(options.addDirs, args[index])
|
|
case strings.HasPrefix(arg, "--add-dir="):
|
|
value := strings.TrimPrefix(arg, "--add-dir=")
|
|
if strings.TrimSpace(value) == "" {
|
|
return options, false, execUsageError{"--add-dir requires a path"}
|
|
}
|
|
options.addDirs = append(options.addDirs, value)
|
|
default:
|
|
return options, false, execUsageError{fmt.Sprintf("unknown serve flag %q", arg)}
|
|
}
|
|
}
|
|
return options, false, nil
|
|
}
|
|
|
|
func writeServeHelp(w io.Writer) error {
|
|
_, err := fmt.Fprint(w, `Usage:
|
|
zero serve --mcp [flags]
|
|
|
|
Starts Zero as an MCP stdio server.
|
|
|
|
Flags:
|
|
--mcp Run the MCP stdio server
|
|
-C, --cwd <path> Set the workspace directory (resources + tools)
|
|
--add-dir <path> Extra resource/tool root (repeatable)
|
|
--allow-unsafe-tools Expose write and shell tools to the MCP host
|
|
-h, --help Show this help
|
|
`)
|
|
return err
|
|
}
|