返回 DeepSeek-Reasonix
install.go
根目录 / internal / plugin / install.go
1 package plugin
2
3 import (
4 "context"
5 "fmt"
6 "strings"
7 )
8
9 // MCPInstallInput is the high-level install request for market entries, pasted
10 // commands/URLs/JSON, and explicit imports. Old AddMCPServer/UpdateMCPServer
11 // remain as compatibility adapters around the same readiness path.
12 type MCPInstallInput struct {
13 RegistryName string `json:"registryName,omitempty"`
14 Definition string `json:"definition,omitempty"`
15 Name string `json:"name,omitempty"`
16 Env map[string]string `json:"env,omitempty"`
17 Headers map[string]string `json:"headers,omitempty"`
18 // Spec is the already-parsed candidate used by in-process callers that
19 // resolved a market entry or argv themselves.
20 Spec Spec `json:"-"`
21 }
22
23 // MCPInstallResult is the user-visible outcome of an install/update transaction.
24 // Only State=="ready" may be phrased as success in CLI/UI copy.
25 type MCPInstallResult struct {
26 Name string `json:"name"`
27 State string `json:"state"` // ready | action_required | issue
28 ToolCount int `json:"toolCount"`
29 Action string `json:"action"` // none | authenticate | authorize | retry
30 Message string `json:"message"`
31 }
32
33 // InstallAndConnect runs initialize + tools/list for a candidate Spec against
34 // the shared Host and returns a readiness result. Callers own durable config,
35 // activation, launcher lock, and schema persistence around this probe so a
36 // failed readiness check does not leave a half-installed entry.
37 func ReadyInstallResult(name string, toolCount int) MCPInstallResult {
38 return MCPInstallResult{
39 Name: strings.TrimSpace(name),
40 State: "ready",
41 ToolCount: toolCount,
42 Action: "none",
43 Message: fmt.Sprintf("%q is available with %d tools", strings.TrimSpace(name), toolCount),
44 }
45 }
46
47 // InstallResultForError maps a handshake error to the stable UI/CLI install
48 // contract. Expected authentication/authorization states are still returned to
49 // callers as errors so low-level users can decide whether durable config should
50 // be retained; high-level transaction owners may turn those states into a
51 // successful RPC response after persistence succeeds.
52 func InstallResultForError(name string, err error) MCPInstallResult {
53 name = strings.TrimSpace(name)
54 if err == nil {
55 return ReadyInstallResult(name, 0)
56 }
57 if requiresLaunchApproval(err) {
58 return MCPInstallResult{Name: name, State: "action_required", Action: "authorize", Message: err.Error()}
59 }
60 msg := err.Error()
61 lower := strings.ToLower(msg)
62 if strings.Contains(lower, "auth") || strings.Contains(lower, "oauth") || strings.Contains(lower, "unauthorized") || strings.Contains(lower, "forbidden") {
63 return MCPInstallResult{Name: name, State: "action_required", Action: "authenticate", Message: msg}
64 }
65 return MCPInstallResult{Name: name, State: "issue", Action: "retry", Message: msg}
66 }
67
68 func (h *Host) InstallAndConnect(ctx context.Context, spec Spec) (MCPInstallResult, error) {
69 return h.InstallAndConnectWithLifecycle(ctx, ctx, spec)
70 }
71
72 // InstallAndConnectWithLifecycle separates ownership of the spawned MCP process
73 // from the readiness-call deadline. Desktop sessions pass their session
74 // lifecycle as lifeCtx and a short request deadline as callCtx; one-shot CLI
75 // probes may use the same context for both.
76 func (h *Host) InstallAndConnectWithLifecycle(lifeCtx, callCtx context.Context, spec Spec) (MCPInstallResult, error) {
77 name := strings.TrimSpace(spec.Name)
78 if name == "" {
79 return MCPInstallResult{State: "issue", Action: "retry", Message: "MCP server name is required"}, fmt.Errorf("mcp install: name is required")
80 }
81 if spec.ProcessMode == "" {
82 spec.ProcessMode = MCPProcessHost
83 }
84 // User/host installs are authorized by the install action itself.
85 if !spec.RequireLaunchApproval {
86 spec.Authorized = true
87 }
88 tools, err := h.EnsureConnectedWithLifecycle(lifeCtx, callCtx, spec, 0)
89 if err != nil {
90 return InstallResultForError(name, err), err
91 }
92 // Persist schema so the next session registers tools without reconnect UX.
93 _ = SaveCachedSchema(spec.Name, CachedSchema{
94 CacheKey: SchemaCacheKey(spec),
95 Capabilities: map[string]bool{"tools": len(tools) > 0},
96 Tools: cacheableToolsOf(tools),
97 })
98 return ReadyInstallResult(name, len(tools)), nil
99 }
100
100 lines GO