返回 DeepSeek-Reasonix
capability_view.go
根目录 / internal / plugin / capability_view.go
1 package plugin
2
3 import (
4 "sort"
5 "strings"
6 )
7
8 // CapabilityViewLayer names for the four-layer capability matrix.
9 const (
10 CapabilityIDProtocol = "protocol"
11 CapabilityIDCore = "core"
12 CapabilityIDInteractive = "interactive"
13 CapabilityIDApps = "apps"
14
15 LayerProtocol = "Protocol Connection"
16 LayerCore = "Core Host"
17 LayerInteractive = "Interactive Host"
18 LayerApps = "Apps Host"
19
20 CapabilityStateSupported = "supported"
21 CapabilityStateNegotiated = "negotiated"
22 CapabilityStateDegraded = "degraded"
23 CapabilityStateUnavailable = "unavailable"
24 )
25
26 // CapabilityView is one row of the public MCP capability matrix: what the host
27 // declares, whether the live sessions turned it into a negotiated capability,
28 // and a human-readable detail. Read-only diagnostics — there is no editable
29 // switch; capabilities follow the frontend's profile.
30 type CapabilityView struct {
31 ID string `json:"id"`
32 Layer string `json:"layer"`
33 State string `json:"state"`
34 Negotiated bool `json:"negotiated"`
35 Detail string `json:"detail"`
36 }
37
38 // CapabilityViews computes the host's four-layer capability matrix from the
39 // profile and the live sessions' negotiation results.
40 func (h *Host) CapabilityViews() []CapabilityView {
41 profile := h.Profile()
42 profileCaps := profile.Capabilities()
43
44 clients := h.snapshotClients()
45 anyConnected := false
46 newestProtocol := ""
47 elicitationLive := false
48 appsLive := false
49 for _, c := range clients {
50 if c.closed.Load() {
51 continue
52 }
53 anyConnected = true
54 if c.protocolVersion > newestProtocol {
55 newestProtocol = c.protocolVersion
56 }
57 if profileCaps.ElicitationForms && c.elicitationUsable() {
58 elicitationLive = true
59 }
60 if profileCaps.AppsUI && c.capabilities.appsUI() {
61 appsLive = true
62 }
63 }
64
65 views := make([]CapabilityView, 0, 4)
66 protocolDetail := "JSON-RPC protocol revision negotiated per server"
67 protocolState := CapabilityStateSupported
68 if anyConnected {
69 protocolState = CapabilityStateNegotiated
70 if newestProtocol != "" {
71 protocolDetail = "Newest negotiated revision: " + newestProtocol
72 }
73 } else {
74 protocolDetail = "No server connected yet; " + protocolDetail
75 }
76 views = append(views, CapabilityView{
77 ID: CapabilityIDProtocol, Layer: LayerProtocol,
78 State: protocolState, Negotiated: anyConnected, Detail: protocolDetail,
79 })
80
81 coreState := CapabilityStateSupported
82 coreDetail := "Tools, prompts, resources over the core protocol"
83 if anyConnected {
84 coreState = CapabilityStateNegotiated
85 }
86 views = append(views, CapabilityView{
87 ID: CapabilityIDCore, Layer: LayerCore,
88 State: coreState, Negotiated: anyConnected, Detail: coreDetail,
89 })
90
91 interactiveState := CapabilityStateUnavailable
92 interactiveDetail := "Host profile " + profile.String() + " declares no elicitation"
93 if profileCaps.ElicitationForms || profileCaps.ElicitationURL {
94 interactiveState = CapabilityStateSupported
95 interactiveDetail = "Form and URL elicitation declared (profile " + profile.String() + ")"
96 if elicitationLive {
97 interactiveState = CapabilityStateNegotiated
98 interactiveDetail = "Elicitation active on at least one compatible session"
99 } else if anyConnected {
100 interactiveDetail = "Declared; no connected session can deliver elicitation yet"
101 }
102 }
103 views = append(views, CapabilityView{
104 ID: CapabilityIDInteractive, Layer: LayerInteractive,
105 State: interactiveState, Negotiated: elicitationLive, Detail: interactiveDetail,
106 })
107
108 appsState := CapabilityStateUnavailable
109 appsDetail := "Host profile " + profile.String() + " declares no Apps extension"
110 if profileCaps.AppsUI {
111 appsState = CapabilityStateSupported
112 appsDetail = "io.modelcontextprotocol/ui declared (profile " + profile.String() + ")"
113 if appsLive {
114 appsState = CapabilityStateNegotiated
115 appsDetail = "Apps extension agreed by at least one server"
116 } else if anyConnected {
117 appsDetail = "Declared; no connected server answered with the Apps extension"
118 }
119 }
120 views = append(views, CapabilityView{
121 ID: CapabilityIDApps, Layer: LayerApps,
122 State: appsState, Negotiated: appsLive, Detail: appsDetail,
123 })
124 return views
125 }
126
127 // fillServerNegotiation stamps one ServerStatus with the host profile and the
128 // per-server negotiated elicitation/Apps state.
129 func fillServerNegotiation(s *ServerStatus, profile HostProfile, c *Client) {
130 s.HostProfile = profile.String()
131 s.ElicitationNegotiated = profile.Capabilities().ElicitationForms && c.elicitationUsable()
132 s.AppsNegotiated = profile.Capabilities().AppsUI && c.capabilities.appsUI()
133 }
134
135 // snapshotClients copies the host's client list under its lock.
136 func (h *Host) snapshotClients() []*Client {
137 h.mu.RLock()
138 defer h.mu.RUnlock()
139 out := make([]*Client, len(h.clients))
140 copy(out, h.clients)
141 return out
142 }
143
144 // FormatCapabilityViews renders the matrix as aligned text for /mcp status.
145 func FormatCapabilityViews(views []CapabilityView) string {
146 widths := [2]int{}
147 for _, v := range views {
148 if len(v.Layer) > widths[0] {
149 widths[0] = len(v.Layer)
150 }
151 if len(v.State) > widths[1] {
152 widths[1] = len(v.State)
153 }
154 }
155 rows := make([]string, 0, len(views))
156 for _, v := range views {
157 rows = append(rows, " "+v.Layer+strings.Repeat(" ", widths[0]-len(v.Layer)+2)+
158 v.State+strings.Repeat(" ", widths[1]-len(v.State)+2)+v.Detail)
159 }
160 return strings.Join(rows, "\n")
161 }
162
163 // SortCapabilityViews orders matrix rows protocol → core → interactive → apps.
164 func SortCapabilityViews(views []CapabilityView) {
165 order := map[string]int{
166 CapabilityIDProtocol: 0, CapabilityIDCore: 1,
167 CapabilityIDInteractive: 2, CapabilityIDApps: 3,
168 }
169 sort.SliceStable(views, func(i, j int) bool {
170 return order[views[i].ID] < order[views[j].ID]
171 })
172 }
173
173 lines GO