返回 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(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 }
36 var caps []capInfo
37 if t.currentToolResultTarget() != nil {
38 caps = append(caps, capInfo{
39 ID: sessionToolResultCapabilityID, Kind: "session", Name: "tool_result", Status: "ready", ReadOnly: true,
40 Description: "Read one bounded page from a complete tool result retained in this agent's current session.",
41 })
42 }
43 catalog := t.currentCatalog()
44 if len(catalog.Entries) > 0 {
45 for _, e := range catalog.Entries {
46 // Servers already have a compact representation below. Keep concrete
47 // MCP tools in the internal catalog for routing, inspect, and known-ID
48 // calls, but do not inject every cached directory into model context.
49 if e.Kind == capability.KindMCPServer || e.Kind == capability.KindMCPTool {
50 continue
51 }
52 // Skip provider-visible core tools — they are already top-level.
53 if e.Kind == capability.KindTool && t.registry != nil && t.registry.ProviderVisible(e.ToolName) {
54 continue
55 }
56 caps = append(caps, capInfo{
57 ID: e.ID,
58 Kind: string(e.Kind),
59 Name: e.Name,
60 Status: string(e.Status),
61 ReadOnly: e.ReadOnly,
62 Description: e.Description,
63 })
64 }
65 }
66 serversJSON, err := t.listServers()
67 if err != nil {
68 return "", err
69 }
70 var serversPayload struct {
71 Servers []listServerInfo `json:"servers"`
72 Note string `json:"note"`
73 }
74 _ = json.Unmarshal([]byte(serversJSON), &serversPayload)
75 total := len(caps) + len(serversPayload.Servers)
76 offset := 0
77 if strings.TrimSpace(cursor) != "" {
78 version, rawOffset, ok := strings.Cut(cursor, ":")
79 if !ok || version != catalog.Fingerprint {
80 return "", fmt.Errorf("list cursor expired because the capability catalog changed; restart without cursor")
81 }
82 parsed, err := strconv.Atoi(rawOffset)
83 if err != nil || parsed < 0 || parsed > total {
84 return "", fmt.Errorf("invalid list cursor; restart without cursor")
85 }
86 offset = parsed
87 }
88 end := min(offset+limit, total)
89 capStart, capEnd := min(offset, len(caps)), min(end, len(caps))
90 page := caps[capStart:capEnd]
91 serverStart := max(0, offset-len(caps))
92 serverEnd := max(0, end-len(caps))
93 serverPage := serversPayload.Servers[serverStart:serverEnd]
94 nextCursor := ""
95 if end < total {
96 nextCursor = catalog.Fingerprint + ":" + strconv.Itoa(end)
97 }
98 payload := map[string]any{
99 "capabilities": page,
100 "servers": serverPage,
101 "catalog_version": catalog.Fingerprint,
102 "next_cursor": nextCursor,
103 "truncated": nextCursor != "",
104 "snapshot_stale": catalog.Stale,
105 "incomplete": catalog.Incomplete,
106 "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.",
107 }
108 if serversPayload.Note != "" {
109 payload["note"] = payload["note"].(string) + " " + serversPayload.Note
110 }
111 b, err := json.MarshalIndent(payload, "", " ")
112 if err != nil {
113 return "", err
114 }
115 return string(b), nil
116 }
117
118 // listServers returns sorted configured MCP server names, status, and
119 // capability IDs without starting servers. Used by Planner discovery when no
120 // specific capability route was provided.
121 func (t *UseCapabilityTool) listServers() (string, error) {
122 configured := t.configuredServers()
123 list := make([]listServerInfo, 0, len(configured))
124 for _, server := range configured {
125 spec := server.spec
126 name := strings.TrimSpace(spec.Name)
127 if name == "" {
128 continue
129 }
130 // Apply stored project grants without process/network side effects so
131 // list status matches resolve/execute authorization.
132 resolved := plugin.ResolveStoredAuthorization(context.Background(), spec)
133 connected := server.enabled && resolved.ServerAuthorized() && t.host != nil && t.host.HasClientForSpec(resolved)
134 status := "configured"
135 if !server.enabled {
136 status = "disabled"
137 } else if connected {
138 status = "ready"
139 } else if t.host != nil {
140 for _, f := range t.host.Failures() {
141 if f.Name == name && strings.TrimSpace(f.Error) != "" {
142 status = "failed"
143 break
144 }
145 }
146 }
147 list = append(list, listServerInfo{
148 Name: name,
149 CapabilityID: "mcp-server:" + name,
150 Status: status,
151 Authorized: resolved.ServerAuthorized(),
152 Connected: connected,
153 })
154 }
155 b, err := json.MarshalIndent(map[string]any{
156 "servers": list,
157 "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.",
158 }, "", " ")
159 if err != nil {
160 return "", err
161 }
162 return string(b), nil
163 }
164
164 lines GO