Skip to content

Commit 56f63cf

Browse files
committed
feat(tray): add "Install server…" menu entry deep-linking to marketplace
Add an "Install server…" item to the Go systray, placed just after "Open Web Control Panel". Clicking it deep-links to the Web UI marketplace (/repositories, Repositories.vue) in the default browser. - client.go: extract OpenWebUIPath(path) from OpenWebUI; new pure appendWebUIPath helper joins a relative path onto the resolved web_ui_url while preserving any existing query string (apikey). - tray.go: new menu item + click handler + handleInstallServer. - tray_stub.go: widen apiClient interface to match. - docs: list the new tray entry in quick-start. Related #MCP-3246
1 parent 3317e9d commit 56f63cf

5 files changed

Lines changed: 155 additions & 12 deletions

File tree

cmd/mcpproxy-tray/internal/api/client.go

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -819,8 +819,15 @@ func (c *Client) SearchTools(query string, limit int) ([]SearchResult, error) {
819819
return searchResults, nil
820820
}
821821

822-
// OpenWebUI opens the web control panel in the default browser
822+
// OpenWebUI opens the web control panel in the default browser.
823823
func (c *Client) OpenWebUI() error {
824+
return c.OpenWebUIPath("")
825+
}
826+
827+
// OpenWebUIPath opens the web control panel in the default browser at the given
828+
// path under the resolved web UI base (e.g. "repositories" for the marketplace
829+
// deep-link). An empty path opens the base control panel, matching OpenWebUI.
830+
func (c *Client) OpenWebUIPath(path string) error {
824831
// Get the actual web UI URL from the /api/v1/info endpoint
825832
// This ensures we use the correct HTTP URL even when connected via socket
826833
resp, err := c.makeRequest("GET", "/api/v1/info", nil)
@@ -841,6 +848,13 @@ func (c *Client) OpenWebUI() error {
841848
return fmt.Errorf("web_ui_url not found in server info")
842849
}
843850

851+
// Append the requested deep-link path (preserving any query string such as
852+
// the apikey the core may already include in web_ui_url).
853+
webUIURL, err = appendWebUIPath(webUIURL, path)
854+
if err != nil {
855+
return fmt.Errorf("failed to build web UI URL: %w", err)
856+
}
857+
844858
// Add API key if not using socket communication
845859
url := webUIURL
846860
if c.apiKey != "" && !strings.HasPrefix(c.baseURL, "unix://") && !strings.HasPrefix(c.baseURL, "npipe://") {
@@ -887,6 +901,27 @@ func (c *Client) OpenWebUI() error {
887901
}
888902
}
889903

904+
// appendWebUIPath joins a relative path onto a resolved web UI base URL,
905+
// preserving any existing query string (e.g. an apikey already embedded by the
906+
// core). An empty path returns the base URL unchanged.
907+
func appendWebUIPath(webUIURL, path string) (string, error) {
908+
if path == "" {
909+
return webUIURL, nil
910+
}
911+
912+
u, err := url.Parse(webUIURL)
913+
if err != nil {
914+
return "", fmt.Errorf("invalid web UI URL %q: %w", webUIURL, err)
915+
}
916+
917+
if !strings.HasSuffix(u.Path, "/") {
918+
u.Path += "/"
919+
}
920+
u.Path += strings.TrimPrefix(path, "/")
921+
922+
return u.String(), nil
923+
}
924+
890925
// makeRequest makes an HTTP request to the API with enhanced error handling and retry logic
891926
func (c *Client) makeRequest(method, path string, _ interface{}) (*Response, error) {
892927
url, err := c.buildURL(path)

cmd/mcpproxy-tray/internal/api/client_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,66 @@ func TestClientGetServers_LogsZeroServersOnlyOnStateChange(t *testing.T) {
6464
assert.Equal(t, 2, recorded.FilterMessage("API returned zero upstream servers").Len())
6565
assert.Equal(t, 1, recorded.FilterMessage("Server state changed").Len())
6666
}
67+
68+
func TestAppendWebUIPath(t *testing.T) {
69+
t.Parallel()
70+
71+
tests := []struct {
72+
name string
73+
webUIURL string
74+
path string
75+
want string
76+
expectErr bool
77+
}{
78+
{
79+
name: "empty path returns base unchanged",
80+
webUIURL: "http://127.0.0.1:8080/ui/",
81+
path: "",
82+
want: "http://127.0.0.1:8080/ui/",
83+
},
84+
{
85+
name: "appends path to trailing-slash base",
86+
webUIURL: "http://127.0.0.1:8080/ui/",
87+
path: "repositories",
88+
want: "http://127.0.0.1:8080/ui/repositories",
89+
},
90+
{
91+
name: "appends path when base lacks trailing slash",
92+
webUIURL: "http://127.0.0.1:8080/ui",
93+
path: "repositories",
94+
want: "http://127.0.0.1:8080/ui/repositories",
95+
},
96+
{
97+
name: "leading slash on path is normalized",
98+
webUIURL: "http://127.0.0.1:8080/ui/",
99+
path: "/repositories",
100+
want: "http://127.0.0.1:8080/ui/repositories",
101+
},
102+
{
103+
name: "preserves existing query string (apikey)",
104+
webUIURL: "http://127.0.0.1:8080/ui/?apikey=secret",
105+
path: "repositories",
106+
want: "http://127.0.0.1:8080/ui/repositories?apikey=secret",
107+
},
108+
{
109+
name: "invalid URL returns error",
110+
webUIURL: "ht tp://bad url",
111+
path: "repositories",
112+
expectErr: true,
113+
},
114+
}
115+
116+
for _, tt := range tests {
117+
tt := tt
118+
t.Run(tt.name, func(t *testing.T) {
119+
t.Parallel()
120+
got, err := appendWebUIPath(tt.webUIURL, tt.path)
121+
if tt.expectErr {
122+
require.Error(t, err)
123+
return
124+
}
125+
require.NoError(t, err)
126+
assert.Equal(t, tt.want, got)
127+
})
128+
}
129+
}

docs/getting-started/quick-start.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ If you installed MCPProxy using the **DMG installer** (macOS) or **Windows insta
5555
:::tip Tray Menu Options
5656
Right-click (or click on macOS) the tray icon to access:
5757
- **Open Web UI** - Launch the management dashboard
58+
- **Install server…** - Open the Web UI marketplace to browse and install MCP servers
5859
- **View Logs** - See server activity
5960
- **Upstream Servers** - View status of all MCP servers, enable/disable individual servers
6061
- **Quit** - Stop MCPProxy completely

internal/tray/tray.go

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,13 @@ type ServerInterface interface {
9393
// App represents the system tray application
9494
type App struct {
9595
server ServerInterface
96-
apiClient interface{ OpenWebUI() error } // API client for web UI access (optional)
97-
logger *zap.SugaredLogger
98-
version string
99-
shutdown func()
96+
apiClient interface {
97+
OpenWebUI() error
98+
OpenWebUIPath(path string) error
99+
} // API client for web UI access (optional)
100+
logger *zap.SugaredLogger
101+
version string
102+
shutdown func()
100103

101104
connectionState ConnectionState
102105
connectionStateMu sync.RWMutex
@@ -128,11 +131,11 @@ type App struct {
128131
autostartItem *systray.MenuItem
129132

130133
// Update notification menu item (hidden until update is available)
131-
updateMenuItem *systray.MenuItem
132-
updateAvailable bool
133-
latestVersion string
134-
latestReleaseURL string
135-
updateCheckMu sync.RWMutex
134+
updateMenuItem *systray.MenuItem
135+
updateAvailable bool
136+
latestVersion string
137+
latestReleaseURL string
138+
updateCheckMu sync.RWMutex
136139

137140
// Config path for opening from menu
138141
configPath string
@@ -161,7 +164,10 @@ func New(server ServerInterface, logger *zap.SugaredLogger, version string, shut
161164
}
162165

163166
// NewWithAPIClient creates a new tray application with an API client for web UI access
164-
func NewWithAPIClient(server ServerInterface, apiClient interface{ OpenWebUI() error }, logger *zap.SugaredLogger, version string, shutdown func()) *App {
167+
func NewWithAPIClient(server ServerInterface, apiClient interface {
168+
OpenWebUI() error
169+
OpenWebUIPath(path string) error
170+
}, logger *zap.SugaredLogger, version string, shutdown func()) *App {
165171
app := &App{
166172
server: server,
167173
apiClient: apiClient,
@@ -574,8 +580,12 @@ func (a *App) onReady() {
574580

575581
// Add Web Control Panel menu item if API client is available
576582
var openWebUIItem *systray.MenuItem
583+
var installServerItem *systray.MenuItem
577584
if a.apiClient != nil {
578585
openWebUIItem = systray.AddMenuItem("Open Web Control Panel", "Open the web control panel in your browser")
586+
// Deep-link into the Web UI marketplace (Repositories.vue) to browse
587+
// and install MCP servers (MCP-37a).
588+
installServerItem = systray.AddMenuItem("Install server…", "Browse and install MCP servers in the web UI marketplace")
579589
}
580590
systray.AddSeparator()
581591

@@ -744,6 +754,20 @@ func (a *App) onReady() {
744754
}()
745755
}
746756

757+
// --- Install Server Click Handler (deep-link to the marketplace) ---
758+
if installServerItem != nil {
759+
go func() {
760+
for {
761+
select {
762+
case <-installServerItem.ClickedCh:
763+
a.handleInstallServer()
764+
case <-a.ctx.Done():
765+
return
766+
}
767+
}
768+
}()
769+
}
770+
747771
// --- Autostart Click Handler (separate goroutine for macOS) ---
748772
if runtime.GOOS == osDarwin && a.autostartItem != nil {
749773
go func() {
@@ -1768,6 +1792,23 @@ func (a *App) handleOpenWebUI() {
17681792
}
17691793
}
17701794

1795+
// handleInstallServer deep-links to the Web UI marketplace (Repositories.vue)
1796+
// so users can browse and install MCP servers (MCP-37a).
1797+
func (a *App) handleInstallServer() {
1798+
if a.apiClient == nil {
1799+
a.logger.Warn("API client not available, cannot open marketplace")
1800+
return
1801+
}
1802+
1803+
a.logger.Info("Opening server marketplace from tray menu")
1804+
1805+
if err := a.apiClient.OpenWebUIPath("repositories"); err != nil {
1806+
a.logger.Error("Failed to open server marketplace", zap.Error(err))
1807+
} else {
1808+
a.logger.Info("Successfully opened server marketplace")
1809+
}
1810+
}
1811+
17711812
// startUpdateChecker starts a background goroutine that periodically checks for updates
17721813
// by querying the core's /api/v1/info endpoint
17731814
func (a *App) startUpdateChecker() {

internal/tray/tray_stub.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,10 @@ func New(_ ServerInterface, logger *zap.SugaredLogger, _ string, _ func()) *App
5151
}
5252

5353
// NewWithAPIClient creates a new tray application with an API client (stub version)
54-
func NewWithAPIClient(_ ServerInterface, _ interface{ OpenWebUI() error }, logger *zap.SugaredLogger, _ string, _ func()) *App {
54+
func NewWithAPIClient(_ ServerInterface, _ interface {
55+
OpenWebUI() error
56+
OpenWebUIPath(path string) error
57+
}, logger *zap.SugaredLogger, _ string, _ func()) *App {
5558
return &App{
5659
logger: logger,
5760
}

0 commit comments

Comments
 (0)