mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-11 13:11:30 -10:00
* fix(mcp): skip an unreachable MCP server at startup instead of aborting RegisterTools connected to every server eagerly and failed the whole batch if any server could not be reached or list its tools — and the caller aborts startup with exit 1 on that error. So a single unreachable MCP server prevented zero from launching at all and disabled every other (working) server. Make registration best-effort across servers but still atomic within a server: each server's tools are validated as a unit before anything commits, and a server that cannot be connected, returns a nameless tool, or conflicts is SKIPPED — recorded on Runtime.Skipped(), not fatal. The caller surfaces each skip as a warning and keeps launching. The "no dead tools" and no-partial-tool-set guarantees are preserved. * feat(mcp): ship keyless Firecrawl as a default web search/scrape server Seed an enabled-by-default Firecrawl MCP server (keyless: 1,000 free credits/month, no API key, no setup) into ResolveMCP before user/project config is merged, so web search and scraping work out of the box for open-source users. A user can override any field (self-hosted URL, bring-your-own-key header) or disable it entirely; `zero mcp disable firecrawl` writes the override even though the default lives in code rather than the config file. The built-in web_fetch tool stays the always-local floor. Firecrawl is AGPL-3.0, but zero only calls it over the network, so the license never reaches zero's own code. README documents disable / self-host / bring-your-own-key / the privacy note that keyless routes through firecrawl.dev. Depends on the resilient-startup change: on an offline machine the default is skipped with a warning, not fatal. * perf(mcp): connect MCP servers concurrently with a per-server startup timeout RegisterTools connected servers serially with a 30s handshake timeout, so startup waited for the SUM of all servers and one slow/unreachable server (e.g. a hosted endpoint blocked by the local network) blocked every other server AND the first model response — often 10-30s of dead time per launch. Connect all servers CONCURRENTLY, each bounded by a per-server ConnectTimeout (default 8s). A server that doesn't connect+list in time is abandoned (its context cancelled to tear down the half-open connection/subprocess) and skipped, so total startup cost is now the slowest REACHABLE server, not the sum. The concurrent phase does only connect+list (no shared state); validation, conflict detection, and registration stay in a deterministic serial phase, so the outcome is identical regardless of completion order. A kept server's context is released at Close — a stdio subprocess is tied to its context, so it must outlive the connect. Pairs with the resilient skip-on-failure behavior already in this branch.
47 lines
1.6 KiB
Go
47 lines
1.6 KiB
Go
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/Gitlawb/zero/internal/config"
|
|
)
|
|
|
|
// `zero mcp disable firecrawl` must work even though firecrawl is a built-in
|
|
// default that is not written to the user's config file until overridden.
|
|
func TestRunMCPDisableSeededFirecrawlDefault(t *testing.T) {
|
|
configPath := filepath.Join(t.TempDir(), "zero", "config.json")
|
|
// A config with no firecrawl entry — the default lives in code, not the file.
|
|
writeMCPCommandRawConfig(t, configPath, `{"activeProvider":"fast"}`)
|
|
|
|
var out, errBuf bytes.Buffer
|
|
code := runWithDeps([]string{"mcp", "disable", "firecrawl", "--json"}, &out, &errBuf, appDeps{
|
|
userConfigPath: func() (string, error) { return configPath, nil },
|
|
})
|
|
if code != exitSuccess {
|
|
t.Fatalf("disable exit=%d stderr=%s", code, errBuf.String())
|
|
}
|
|
var payload struct {
|
|
ServerName string `json:"serverName"`
|
|
Disabled bool `json:"disabled"`
|
|
Changed bool `json:"changed"`
|
|
}
|
|
if err := json.Unmarshal(out.Bytes(), &payload); err != nil {
|
|
t.Fatalf("decode disable JSON: %v\n%s", err, out.String())
|
|
}
|
|
if payload.ServerName != "firecrawl" || !payload.Disabled || !payload.Changed {
|
|
t.Fatalf("disable payload = %#v, want firecrawl disabled+changed", payload)
|
|
}
|
|
|
|
// End-to-end: resolving that config now turns the default off.
|
|
cfg, err := config.ResolveMCP(config.ResolveOptions{UserConfigPath: configPath})
|
|
if err != nil {
|
|
t.Fatalf("ResolveMCP: %v", err)
|
|
}
|
|
if !cfg.Servers["firecrawl"].Disabled {
|
|
t.Fatal("expected `mcp disable firecrawl` to turn the seeded default off in the resolved config")
|
|
}
|
|
}
|