Files
DeepSeek-Reasonix/internal/plugin/install.go
T
SivanCola 31f037d07a feat(mcp): make trusted servers zero-config and reliable / 让可信 MCP 服务零配置且可靠可用 (#6829)
* fix(mcp): make trusted servers zero-config and reliable

Run authorized stdio servers on the host by default while retaining private state directories, bounded lifecycle cleanup, lazy schema restoration, and single-flight startup.

Add durable activation, transactional readiness and rollback, CLI candidate probing, and inherited MCP access for child agents. Include a real chrome-devtools-mcp release smoke test.

* feat(desktop): reduce MCP install to one input

Accept a command, URL, or JSON definition in the common install path, surface structured readiness and authentication states, and keep transport and timeout controls in the advanced edit flow.

* feat(desktop): unify MCP quick and advanced setup

Problem: The zero-config MCP installer removed useful configuration controls and left the add-server page feeling too sparse.

Design: Keep quick install as the default, restore manual and JSON modes in the same editor, carry pasted definitions into advanced modes, and make the automatic install outcomes explicit.

Verification: npm run typecheck; npx tsx src/__tests__/capabilities-panel-actions.test.ts; npm run check:css; npm run build; git diff --check.

* fix(plugin): remove obsolete deferred wrapper

Problem: golangci-lint reported Host.addDeferred as an unused private method.

Root cause: Lazy startup now calls EnsureConnectedWithLifecycle directly, but the old wrapper remained after the single-flight migration.

Fix: Remove the dead wrapper and its stale comment without changing runtime behavior.

Verification: go test ./internal/plugin; go test -race ./internal/plugin; scripts/check-cache-impact.sh; git diff --check.

* fix(desktop): remove obsolete MCP intent helper

Problem: The desktop golangci-lint job reported mcpStartIntent as an unused private function.

Root cause: Legacy startIntent fields are now derived in withPluginConfigInWorkspace, but the previous helper remained after the activation-state migration.

Fix: Remove the dead helper while preserving the derived compatibility fields.

Verification: cd desktop && go test ./...; cd desktop && go vet ./...; scripts/check-cache-impact.sh; git diff --check.

* fix(mcp): preserve trust and on-demand boundaries

Problem: Explicit subagent tool allowlists could be widened by MCP inheritance, host-session readiness probes owned successful stdio processes, durable enable still connected eagerly, activation updates could clobber each other, quick names followed trailing arguments, and Windows asserted POSIX mode bits.

Root cause: MCP inheritance bypassed the normal registry filter; the short probe context was reused as process lifetime; enable reused the live-connect API; activation stores only locked per instance; name derivation scanned from the command tail; and the permission test was not platform-aware.

Fix: Keep explicit allowlists authoritative while preserving default MCP inheritance, split ExtraPlugin call and lifetime contexts, add cache-backed on-demand registration, serialize activation transactions with a cross-process lock, derive quick names from launcher operands in desktop and CLI, and scope mode-bit assertions to POSIX.

Verification: go test -race ./internal/agent ./internal/boot ./internal/control ./internal/config; env -u DEEPSEEK_API_KEY go test ./...; cd desktop and run go test ./... plus go vet ./...; frontend MCP actions, typecheck, build, and CSS checks; go vet ./...; ./scripts/cache-guard.sh; git diff --check.
2026-07-22 21:42:47 +08:00

100 lines
4.2 KiB
Go

package plugin
import (
"context"
"fmt"
"strings"
)
// MCPInstallInput is the high-level install request for market entries, pasted
// commands/URLs/JSON, and explicit imports. Old AddMCPServer/UpdateMCPServer
// remain as compatibility adapters around the same readiness path.
type MCPInstallInput struct {
RegistryName string `json:"registryName,omitempty"`
Definition string `json:"definition,omitempty"`
Name string `json:"name,omitempty"`
Env map[string]string `json:"env,omitempty"`
Headers map[string]string `json:"headers,omitempty"`
// Spec is the already-parsed candidate used by in-process callers that
// resolved a market entry or argv themselves.
Spec Spec `json:"-"`
}
// MCPInstallResult is the user-visible outcome of an install/update transaction.
// Only State=="ready" may be phrased as success in CLI/UI copy.
type MCPInstallResult struct {
Name string `json:"name"`
State string `json:"state"` // ready | action_required | issue
ToolCount int `json:"toolCount"`
Action string `json:"action"` // none | authenticate | authorize | retry
Message string `json:"message"`
}
// InstallAndConnect runs initialize + tools/list for a candidate Spec against
// the shared Host and returns a readiness result. Callers own durable config,
// activation, launcher lock, and schema persistence around this probe so a
// failed readiness check does not leave a half-installed entry.
func ReadyInstallResult(name string, toolCount int) MCPInstallResult {
return MCPInstallResult{
Name: strings.TrimSpace(name),
State: "ready",
ToolCount: toolCount,
Action: "none",
Message: fmt.Sprintf("%q is available with %d tools", strings.TrimSpace(name), toolCount),
}
}
// InstallResultForError maps a handshake error to the stable UI/CLI install
// contract. Expected authentication/authorization states are still returned to
// callers as errors so low-level users can decide whether durable config should
// be retained; high-level transaction owners may turn those states into a
// successful RPC response after persistence succeeds.
func InstallResultForError(name string, err error) MCPInstallResult {
name = strings.TrimSpace(name)
if err == nil {
return ReadyInstallResult(name, 0)
}
if requiresLaunchApproval(err) {
return MCPInstallResult{Name: name, State: "action_required", Action: "authorize", Message: err.Error()}
}
msg := err.Error()
lower := strings.ToLower(msg)
if strings.Contains(lower, "auth") || strings.Contains(lower, "oauth") || strings.Contains(lower, "unauthorized") || strings.Contains(lower, "forbidden") {
return MCPInstallResult{Name: name, State: "action_required", Action: "authenticate", Message: msg}
}
return MCPInstallResult{Name: name, State: "issue", Action: "retry", Message: msg}
}
func (h *Host) InstallAndConnect(ctx context.Context, spec Spec) (MCPInstallResult, error) {
return h.InstallAndConnectWithLifecycle(ctx, ctx, spec)
}
// InstallAndConnectWithLifecycle separates ownership of the spawned MCP process
// from the readiness-call deadline. Desktop sessions pass their session
// lifecycle as lifeCtx and a short request deadline as callCtx; one-shot CLI
// probes may use the same context for both.
func (h *Host) InstallAndConnectWithLifecycle(lifeCtx, callCtx context.Context, spec Spec) (MCPInstallResult, error) {
name := strings.TrimSpace(spec.Name)
if name == "" {
return MCPInstallResult{State: "issue", Action: "retry", Message: "MCP server name is required"}, fmt.Errorf("mcp install: name is required")
}
if spec.ProcessMode == "" {
spec.ProcessMode = MCPProcessHost
}
// User/host installs are authorized by the install action itself.
if !spec.RequireLaunchApproval {
spec.Authorized = true
}
tools, err := h.EnsureConnectedWithLifecycle(lifeCtx, callCtx, spec, 0)
if err != nil {
return InstallResultForError(name, err), err
}
// Persist schema so the next session registers tools without reconnect UX.
_ = SaveCachedSchema(spec.Name, CachedSchema{
CacheKey: SchemaCacheKey(spec),
Capabilities: map[string]bool{"tools": len(tools) > 0},
Tools: cacheableToolsOf(tools),
})
return ReadyInstallResult(name, len(tools)), nil
}