diff --git a/docs/framework/mcp.md b/docs/framework/mcp.md index cf437b20..7f4c0047 100644 --- a/docs/framework/mcp.md +++ b/docs/framework/mcp.md @@ -65,6 +65,13 @@ The MCP service exposes numerous tools organized by category: | `webview_navigate` | Navigate to URL | | `webview_console` | Get console logs | +### Browser + +| Tool | Description | +|------|-------------| +| `browser_open_url` | Open a URL in the default system browser | +| `browser_open_file` | Open a file with the system default application | + ### Process Management | Tool | Description | diff --git a/pkg/display/FEATURES.md b/pkg/display/FEATURES.md index 4306b500..ad459800 100644 --- a/pkg/display/FEATURES.md +++ b/pkg/display/FEATURES.md @@ -76,6 +76,10 @@ This document tracks the implementation of display server features that enable A - [x] `webview_eval` - Execute JavaScript and return result - [x] `webview_list` - List all webview windows +### Browser +- [x] `browser_open_url` - Open a URL in the default system browser +- [x] `browser_open_file` - Open a file in the system default application + ### Console & Errors - [x] `webview_console` - Get console messages (log, warn, error, info) - [x] `webview_errors` - Get structured JS errors with stack traces diff --git a/pkg/mcp/mcp_test.go b/pkg/mcp/mcp_test.go index 2e2006a7..cab0c27a 100644 --- a/pkg/mcp/mcp_test.go +++ b/pkg/mcp/mcp_test.go @@ -7,6 +7,7 @@ import ( "testing" core "dappco.re/go/core" + "forge.lthn.ai/core/gui/pkg/browser" "forge.lthn.ai/core/gui/pkg/clipboard" "forge.lthn.ai/core/gui/pkg/dialog" "forge.lthn.ai/core/gui/pkg/events" @@ -72,6 +73,11 @@ func TestMCP_Bad_NoServices(t *testing.T) { type manifestScreenPlatform struct{} +type manifestBrowserPlatform struct { + lastURL string + lastPath string +} + func (manifestScreenPlatform) GetAll() []screen.Screen { return []screen.Screen{{ ID: "1", Name: "Primary", IsPrimary: true, @@ -106,6 +112,34 @@ func TestSubsystem_Good_CallTool_LayoutSuggest(t *testing.T) { assert.Contains(t, result, "left-right") } +func (m *manifestBrowserPlatform) OpenURL(url string) error { + m.lastURL = url + return nil +} + +func (m *manifestBrowserPlatform) OpenFile(path string) error { + m.lastPath = path + return nil +} + +func TestSubsystem_Good_CallTool_BrowserOpenFile(t *testing.T) { + browserPlatform := &manifestBrowserPlatform{} + c := core.New( + core.WithService(browser.Register(browserPlatform)), + core.WithServiceLock(), + ) + require.True(t, c.ServiceStartup(context.Background(), nil).OK) + + sub := New(c) + server := mcp.NewServer(&mcp.Implementation{Name: "test", Version: "0.1.0"}, nil) + sub.RegisterTools(server) + + result, err := sub.CallTool(context.Background(), "browser_open_file", map[string]any{"path": "/tmp/readme.txt"}) + require.NoError(t, err) + assert.Contains(t, result, "success") + assert.Equal(t, "/tmp/readme.txt", browserPlatform.lastPath) +} + type aliasDialogPlatform struct { last dialog.MessageDialogOptions } diff --git a/pkg/mcp/tools_browser.go b/pkg/mcp/tools_browser.go index 6be5e7f7..b828948b 100644 --- a/pkg/mcp/tools_browser.go +++ b/pkg/mcp/tools_browser.go @@ -30,8 +30,31 @@ func (s *Subsystem) browserOpenURL(_ context.Context, _ *mcp.CallToolRequest, in return nil, BrowserOpenURLOutput{Success: true}, nil } +// --- browser_open_file --- + +type BrowserOpenFileInput struct { + Path string `json:"path"` +} +type BrowserOpenFileOutput struct { + Success bool `json:"success"` +} + +func (s *Subsystem) browserOpenFile(_ context.Context, _ *mcp.CallToolRequest, input BrowserOpenFileInput) (*mcp.CallToolResult, BrowserOpenFileOutput, error) { + r := s.core.Action("browser.openFile").Run(context.Background(), core.NewOptions( + core.Option{Key: "path", Value: input.Path}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, BrowserOpenFileOutput{}, e + } + return nil, BrowserOpenFileOutput{}, nil + } + return nil, BrowserOpenFileOutput{Success: true}, nil +} + // --- Registration --- func (s *Subsystem) registerBrowserTools(server *mcp.Server) { addTool(s, server, &mcp.Tool{Name: "browser_open_url", Description: "Open a URL in the default system browser"}, s.browserOpenURL) + addTool(s, server, &mcp.Tool{Name: "browser_open_file", Description: "Open a file in the system default application"}, s.browserOpenFile) }