返回 DeepSeek-Reasonix
usecapability_list.go
根目录 / internal / agent / usecapability_list.go
1 package agent
2
3 import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "strconv"
8 "strings"
9
10 "reasonix/internal/capability"
11 "reasonix/internal/plugin"
12 )
13
14 // listServerInfo is one configured MCP server entry returned by action=list.
15 // It never starts a server or opens a network connection.
16 type listServerInfo struct {
17 Name string `json:"name"`
18 CapabilityID string `json:"capability_id"`
19 Status string `json:"status"`
20 Authorized bool `json:"authorized"`
21 Connected bool `json:"connected"`
22 }
23
24 func (t *UseCapabilityTool) listCapabilitiesPage(ctx context.Context, limit int, cursor string) (string, error) {
25 if limit == 0 {
26 limit = 50
27 }
28 type capInfo struct {
29 ID string `json:"id"`
30 Kind string `json:"kind"`
31 Name string `json:"name"`
32 Status string `json:"status,omitempty"`
33 ReadOnly bool `json:"read_only,omitempty"`
34 Description string `json:"description,omitempty"`
35 UnavailableReason string `json:"unavailable_reason,omitempty"`
36 }
37 var caps []capInfo
38 if t.currentToolResultTarget() != nil {
39 caps = append(caps, capInfo{
40 ID: sessionToolResultCapabilityID, Kind: "session", Name: "tool_result", Status: "ready", ReadOnly: true,
41 Description: "Read one bounded page from a complete tool result retained in this agent's current session.",
42 })
43 }
44 catalog := t.currentCatalog()
45 if len(catalog.Entries) > 0 {
46 for _, e := range catalog.Entries {
47 e = t.contextualEntry(ctx, e)
48 // Servers already have a compact representation below. Keep concrete
49 // MCP tools in the internal catalog for routing, inspect, and known-ID
50 // calls, but do not inject every cached directory into model context.
51 if e.Kind == capability.KindMCPServer || e.Kind == capability.KindMCPTool {
52 continue
53 }
54 // Skip provider-visible core tools — they are already top-level.
55 if e.Kind == capability.KindTool && t.registry != nil && t.registry.ProviderVisible(e.ToolName) {
56 continue
57 }
58 caps = append(caps, capInfo{
59 ID: e.ID,
60 Kind: string(e.Kind),
61 Name: e.Name,
62 Status: string(e.Status),
63 ReadOnly: e.ReadOnly,
64 Description: e.Description,
65 UnavailableReason: e.FailureReason,
66 })
67 }
68 }
69 serversJSON, err := t.listServers()
70 if err != nil {
71 return "", err
72 }
73 var serversPayload struct {
74 Servers []listServerInfo `json:"servers"`
75 Note string `json:"note"`
76 }
77 _ = json.Unmarshal([]byte(serversJSON), &serversPayload)
78 total := len(caps) + len(serversPayload.Servers)
79 offset := 0
80 if strings.TrimSpace(cursor) != "" {
81 version, rawOffset, ok := strings.Cut(cursor, ":")
82 if !ok || version != catalog.Fingerprint {
83 return "", fmt.Errorf("list cursor expired because the capability catalog changed; restart without cursor")
84 }
85 parsed, err := strconv.Atoi(rawOffset)
86 if err != nil || parsed < 0 || parsed > total {
87 return "", fmt.Errorf("invalid list cursor; restart without cursor")
88 }
89 offset = parsed
90 }
91 end := min(offset+limit, total)
92 capStart, capEnd := min(offset, len(caps)), min(end, len(caps))
93 page := caps[capStart:capEnd]
94 serverStart := max(0, offset-len(caps))
95 serverEnd := max(0, end-len(caps))
96 serverPage := serversPayload.Servers[serverStart:serverEnd]
97 nextCursor := ""
98 if end < total {
99 nextCursor = catalog.Fingerprint + ":" + strconv.Itoa(end)
100 }
101 payload := map[string]any{
102 "capabilities": page,
103 "servers": serverPage,
104 "catalog_version": catalog.Fingerprint,
105 "next_cursor": nextCursor,
106 "truncated": nextCursor != "",
107 "snapshot_stale": catalog.Stale,
108 "incomplete": catalog.Incomplete,
109 "note": "This page contains at most limit entries across capabilities and MCP server summaries. Call action=inspect with capability_id=mcp-server:<name> to list one enabled server's tools without starting it, or action=call with a concrete capability_id to invoke a non-core tool, skill, MCP tool, or other catalog entry without changing the provider tool schema.",
110 }
111 if serversPayload.Note != "" {
112 payload["note"] = payload["note"].(string) + " " + serversPayload.Note
113 }
114 b, err := json.MarshalIndent(payload, "", " ")
115 if err != nil {
116 return "", err
117 }
118 return string(b), nil
119 }
120
121 // listServers returns sorted configured MCP server names, status, and
122 // capability IDs without starting servers. Used by Planner discovery when no
123 // specific capability route was provided.
124 func (t *UseCapabilityTool) listServers() (string, error) {
125 configured := t.configuredServers()
126 list := make([]listServerInfo, 0, len(configured))
127 for _, server := range configured {
128 spec := server.spec
129 name := strings.TrimSpace(spec.Name)
130 if name == "" {
131 continue
132 }
133 // Apply stored project grants without process/network side effects so
134 // list status matches resolve/execute authorization.
135 resolved := plugin.ResolveStoredAuthorization(context.Background(), spec)
136 connected := server.enabled && resolved.ServerAuthorized() && t.host != nil && t.host.HasClientForSpec(resolved)
137 status := "configured"
138 if !server.enabled {
139 status = server.decision.Code()
140 } else if connected {
141 status = "ready"
142 } else if t.host != nil {
143 for _, f := range t.host.Failures() {
144 if f.Name == name && strings.TrimSpace(f.Error) != "" {
145 status = "failed"
146 break
147 }
148 }
149 }
150 list = append(list, listServerInfo{
151 Name: name,
152 CapabilityID: "mcp-server:" + name,
153 Status: status,
154 Authorized: resolved.ServerAuthorized(),
155 Connected: connected,
156 })
157 }
158 b, err := json.MarshalIndent(map[string]any{
159 "servers": list,
160 "note": "list does not start MCP servers. Call action=call on mcp-server:<name> to connect after authorization, or mcp-tool:<server>/<tool> for a concrete tool.",
161 }, "", " ")
162 if err != nil {
163 return "", err
164 }
165 return string(b), nil
166 }
167
167 lines GO