Skip to content

Commit 95dc3d4

Browse files
committed
Implement P5: Connect Tray to REST/SSE
- Create API client for /api/v1 endpoints with SSE support - Add adapter pattern to bridge API client with ServerInterface - Update tray to communicate via HTTP API instead of direct imports - Add 'Open Web Control Panel' menu item - Remove direct core package dependencies from tray - Achieve clean separation: tray ↔ HTTP API ↔ core server - Maintain real-time updates and full server management functionality
1 parent f14bb4b commit 95dc3d4

5 files changed

Lines changed: 758 additions & 14 deletions

File tree

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
//go:build darwin
2+
3+
package api
4+
5+
import (
6+
"context"
7+
"fmt"
8+
)
9+
10+
// ServerAdapter adapts the API client to the ServerInterface expected by the tray
11+
type ServerAdapter struct {
12+
client *Client
13+
}
14+
15+
// NewServerAdapter creates a new server adapter
16+
func NewServerAdapter(client *Client) *ServerAdapter {
17+
return &ServerAdapter{
18+
client: client,
19+
}
20+
}
21+
22+
// IsRunning checks if the server is running via API
23+
func (a *ServerAdapter) IsRunning() bool {
24+
servers, err := a.client.GetServers()
25+
if err != nil {
26+
return false
27+
}
28+
29+
// If we can fetch servers, the API is responsive
30+
return len(servers) >= 0
31+
}
32+
33+
// GetListenAddress returns the listen address (hardcoded since API is available)
34+
func (a *ServerAdapter) GetListenAddress() string {
35+
// Since we can reach the API, we know it's listening on this address
36+
return ":8080"
37+
}
38+
39+
// GetUpstreamStats returns upstream server statistics
40+
func (a *ServerAdapter) GetUpstreamStats() map[string]interface{} {
41+
servers, err := a.client.GetServers()
42+
if err != nil {
43+
return map[string]interface{}{
44+
"connected_servers": 0,
45+
"total_servers": 0,
46+
"total_tools": 0,
47+
}
48+
}
49+
50+
connectedCount := 0
51+
totalTools := 0
52+
for _, server := range servers {
53+
if server.Connected {
54+
connectedCount++
55+
}
56+
totalTools += server.ToolCount
57+
}
58+
59+
return map[string]interface{}{
60+
"connected_servers": connectedCount,
61+
"total_servers": len(servers),
62+
"total_tools": totalTools,
63+
}
64+
}
65+
66+
// StartServer is not supported via API (server is already running)
67+
func (a *ServerAdapter) StartServer(ctx context.Context) error {
68+
return fmt.Errorf("StartServer not supported via API - server is already running")
69+
}
70+
71+
// StopServer is not supported via API (would break tray communication)
72+
func (a *ServerAdapter) StopServer() error {
73+
return fmt.Errorf("StopServer not supported via API - would break tray communication")
74+
}
75+
76+
// GetStatus returns the current server status
77+
func (a *ServerAdapter) GetStatus() interface{} {
78+
servers, err := a.client.GetServers()
79+
if err != nil {
80+
return map[string]interface{}{
81+
"phase": "Error",
82+
"message": fmt.Sprintf("API error: %v", err),
83+
}
84+
}
85+
86+
connectedCount := 0
87+
for _, server := range servers {
88+
if server.Connected {
89+
connectedCount++
90+
}
91+
}
92+
93+
return map[string]interface{}{
94+
"phase": "Running",
95+
"message": fmt.Sprintf("API connected - %d servers", len(servers)),
96+
"connected_servers": connectedCount,
97+
"total_servers": len(servers),
98+
}
99+
}
100+
101+
// StatusChannel returns the channel for status updates from SSE
102+
func (a *ServerAdapter) StatusChannel() <-chan interface{} {
103+
// Convert the typed channel to interface{} channel
104+
ch := make(chan interface{}, 10)
105+
106+
go func() {
107+
defer close(ch)
108+
for update := range a.client.StatusChannel() {
109+
// Convert StatusUpdate to the format expected by tray
110+
status := map[string]interface{}{
111+
"phase": "Running",
112+
"message": "Connected via API",
113+
"running": update.Running,
114+
"listen_addr": update.ListenAddr,
115+
"upstream_stats": update.UpstreamStats,
116+
"timestamp": update.Timestamp,
117+
}
118+
119+
select {
120+
case ch <- status:
121+
default:
122+
// Channel full, skip this update
123+
}
124+
}
125+
}()
126+
127+
return ch
128+
}
129+
130+
// GetQuarantinedServers returns quarantined servers
131+
func (a *ServerAdapter) GetQuarantinedServers() ([]map[string]interface{}, error) {
132+
servers, err := a.client.GetServers()
133+
if err != nil {
134+
return nil, err
135+
}
136+
137+
var quarantined []map[string]interface{}
138+
for _, server := range servers {
139+
if server.Quarantined {
140+
quarantined = append(quarantined, map[string]interface{}{
141+
"name": server.Name,
142+
"url": server.URL,
143+
"command": server.Command,
144+
"protocol": server.Protocol,
145+
"enabled": server.Enabled,
146+
"quarantined": server.Quarantined,
147+
})
148+
}
149+
}
150+
151+
return quarantined, nil
152+
}
153+
154+
// UnquarantineServer removes a server from quarantine
155+
func (a *ServerAdapter) UnquarantineServer(serverName string) error {
156+
// This functionality is not available in the current API
157+
// Would need to be added to the API first
158+
return fmt.Errorf("UnquarantineServer not yet supported via API")
159+
}
160+
161+
// EnableServer enables or disables a server
162+
func (a *ServerAdapter) EnableServer(serverName string, enabled bool) error {
163+
return a.client.EnableServer(serverName, enabled)
164+
}
165+
166+
// QuarantineServer sets quarantine status for a server
167+
func (a *ServerAdapter) QuarantineServer(serverName string, quarantined bool) error {
168+
// This functionality is not available in the current API
169+
// Would need to be added to the API first
170+
return fmt.Errorf("QuarantineServer not yet supported via API")
171+
}
172+
173+
// GetAllServers returns all servers
174+
func (a *ServerAdapter) GetAllServers() ([]map[string]interface{}, error) {
175+
servers, err := a.client.GetServers()
176+
if err != nil {
177+
return nil, err
178+
}
179+
180+
var result []map[string]interface{}
181+
for _, server := range servers {
182+
result = append(result, map[string]interface{}{
183+
"name": server.Name,
184+
"url": server.URL,
185+
"command": server.Command,
186+
"protocol": server.Protocol,
187+
"enabled": server.Enabled,
188+
"quarantined": server.Quarantined,
189+
"connected": server.Connected,
190+
"connecting": server.Connecting,
191+
"tool_count": server.ToolCount,
192+
"last_error": server.LastError,
193+
})
194+
}
195+
196+
return result, nil
197+
}
198+
199+
// ReloadConfiguration reloads the configuration
200+
func (a *ServerAdapter) ReloadConfiguration() error {
201+
// This functionality is not available in the current API
202+
// Would need to be added to the API first
203+
return fmt.Errorf("ReloadConfiguration not yet supported via API")
204+
}
205+
206+
// GetConfigPath returns the configuration file path
207+
func (a *ServerAdapter) GetConfigPath() string {
208+
return "~/.mcpproxy/mcp_config.json"
209+
}
210+
211+
// GetLogDir returns the log directory path
212+
func (a *ServerAdapter) GetLogDir() string {
213+
return "~/.mcpproxy/logs"
214+
}
215+
216+
// TriggerOAuthLogin triggers OAuth login for a server
217+
func (a *ServerAdapter) TriggerOAuthLogin(serverName string) error {
218+
return a.client.TriggerOAuthLogin(serverName)
219+
}

0 commit comments

Comments
 (0)