From 710bd4f7b57e7cc5f52beb20ab5014864466999f Mon Sep 17 00:00:00 2001 From: Snider Date: Wed, 15 Apr 2026 20:53:13 +0100 Subject: [PATCH] Add marketplace display and MCP actions --- pkg/display/display.go | 9 +++ pkg/display/marketplace.go | 123 ++++++++++++++++++++++++++++ pkg/mcp/subsystem.go | 1 + pkg/mcp/tools_marketplace.go | 151 +++++++++++++++++++++++++++++++++++ 4 files changed, 284 insertions(+) create mode 100644 pkg/display/marketplace.go create mode 100644 pkg/mcp/tools_marketplace.go diff --git a/pkg/display/display.go b/pkg/display/display.go index 0af676dd..e26faaac 100644 --- a/pkg/display/display.go +++ b/pkg/display/display.go @@ -151,6 +151,7 @@ func (s *Service) OnStartup(_ context.Context) core.Result { return core.Result{Value: s.networkState(), OK: true} }) s.registerBackgroundActions() + s.registerMarketplaceActions() s.registerSidecarActions() s.registerDefaultSchemes() @@ -529,6 +530,14 @@ func (s *Service) handleWSMessage(msg WSMessage) core.Result { return c.Action("gui.chat.thinking.append").Run(ctx, wsOptions(msg.Data)) case "chat:thinking:end": return c.Action("gui.chat.thinking.end").Run(ctx, wsOptions(msg.Data)) + case "marketplace:list": + return c.Action("display.marketplace.list").Run(ctx, wsOptions(msg.Data)) + case "marketplace:fetch": + return c.Action("display.marketplace.fetch").Run(ctx, wsOptions(msg.Data)) + case "marketplace:verify": + return c.Action("display.marketplace.verify").Run(ctx, wsOptions(msg.Data)) + case "marketplace:install": + return c.Action("display.marketplace.install").Run(ctx, wsOptions(msg.Data)) case "keybinding:add": accelerator, _ := msg.Data["accelerator"].(string) description, _ := msg.Data["description"].(string) diff --git a/pkg/display/marketplace.go b/pkg/display/marketplace.go new file mode 100644 index 00000000..c63f4b43 --- /dev/null +++ b/pkg/display/marketplace.go @@ -0,0 +1,123 @@ +package display + +import ( + "context" + "net/http" + "os" + "path/filepath" + "strings" + + core "dappco.re/go/core" + coreerr "dappco.re/go/core/log" + "forge.lthn.ai/core/gui/pkg/marketplace" +) + +type marketplaceListInput struct { + RegistryURL string `json:"url"` +} + +type marketplaceFetchInput struct { + ManifestURL string `json:"url"` +} + +type marketplaceInstallInput struct { + ManifestURL string `json:"url"` + InstallDir string `json:"install_dir,omitempty"` + GitBinary string `json:"git_binary,omitempty"` +} + +func (s *Service) registerMarketplaceActions() { + s.Core().Action("display.marketplace.list", func(ctx context.Context, opts core.Options) core.Result { + input := marketplaceListInput{RegistryURL: marketplaceRegistryURL(opts)} + if strings.TrimSpace(input.RegistryURL) == "" { + return core.Result{Value: coreerr.E("display.marketplace.list", "registry url is required", nil), OK: false} + } + installer := marketplace.Installer{HTTPClient: http.DefaultClient} + manifests, err := installer.List(ctx, input.RegistryURL) + if err != nil { + return core.Result{Value: err, OK: false} + } + return core.Result{Value: map[string]any{ + "registry_url": input.RegistryURL, + "manifests": manifests, + }, OK: true} + }) + + s.Core().Action("display.marketplace.fetch", func(ctx context.Context, opts core.Options) core.Result { + input := marketplaceFetchInput{ManifestURL: strings.TrimSpace(opts.String("url"))} + if input.ManifestURL == "" { + return core.Result{Value: coreerr.E("display.marketplace.fetch", "manifest url is required", nil), OK: false} + } + installer := marketplace.Installer{HTTPClient: http.DefaultClient} + manifest, err := installer.FetchManifest(ctx, input.ManifestURL) + if err != nil { + return core.Result{Value: err, OK: false} + } + return core.Result{Value: manifest, OK: true} + }) + + s.Core().Action("display.marketplace.verify", func(ctx context.Context, opts core.Options) core.Result { + input := marketplaceFetchInput{ManifestURL: strings.TrimSpace(opts.String("url"))} + if input.ManifestURL == "" { + return core.Result{Value: coreerr.E("display.marketplace.verify", "manifest url is required", nil), OK: false} + } + installer := marketplace.Installer{HTTPClient: http.DefaultClient} + manifest, err := installer.Verify(ctx, input.ManifestURL) + if err != nil { + return core.Result{Value: err, OK: false} + } + return core.Result{Value: map[string]any{ + "manifest": manifest, + "digest": marketplace.DigestManifest(manifest), + }, OK: true} + }) + + s.Core().Action("display.marketplace.install", func(ctx context.Context, opts core.Options) core.Result { + input := marketplaceInstallInput{ + ManifestURL: strings.TrimSpace(opts.String("url")), + InstallDir: strings.TrimSpace(opts.String("install_dir")), + GitBinary: strings.TrimSpace(opts.String("git_binary")), + } + if input.ManifestURL == "" { + return core.Result{Value: coreerr.E("display.marketplace.install", "manifest url is required", nil), OK: false} + } + + installer := marketplace.Installer{ + HTTPClient: http.DefaultClient, + GitBinary: input.GitBinary, + InstallDir: marketplaceInstallRoot(input.InstallDir), + } + manifest, err := installer.Verify(ctx, input.ManifestURL) + if err != nil { + return core.Result{Value: err, OK: false} + } + targetDir, err := installer.Install(ctx, manifest) + if err != nil { + return core.Result{Value: err, OK: false} + } + return core.Result{Value: map[string]any{ + "manifest": manifest, + "digest": marketplace.DigestManifest(manifest), + "target_dir": targetDir, + "install_dir": installer.InstallDir, + }, OK: true} + }) +} + +func marketplaceRegistryURL(opts core.Options) string { + if url := strings.TrimSpace(opts.String("url")); url != "" { + return url + } + return strings.TrimSpace(core.Env("CORE_MARKETPLACE_REGISTRY_URL")) +} + +func marketplaceInstallRoot(raw string) string { + if trimmed := strings.TrimSpace(raw); trimmed != "" { + return trimmed + } + home := strings.TrimSpace(core.Env("DIR_HOME")) + if home == "" { + return filepath.Join(os.TempDir(), "core", "apps") + } + return filepath.Join(home, ".core", "apps") +} diff --git a/pkg/mcp/subsystem.go b/pkg/mcp/subsystem.go index 61fd20ed..62a87cb4 100644 --- a/pkg/mcp/subsystem.go +++ b/pkg/mcp/subsystem.go @@ -59,6 +59,7 @@ func (s *Subsystem) RegisterTools(server *mcp.Server) { s.registerKeybindingTools(server) s.registerDockTools(server) s.registerLifecycleTools(server) + s.registerMarketplaceTools(server) s.registerEventsTools(server) s.registerMenuTools(server) } diff --git a/pkg/mcp/tools_marketplace.go b/pkg/mcp/tools_marketplace.go new file mode 100644 index 00000000..66344d26 --- /dev/null +++ b/pkg/mcp/tools_marketplace.go @@ -0,0 +1,151 @@ +// pkg/mcp/tools_marketplace.go +package mcp + +import ( + "context" + + core "dappco.re/go/core" + coreerr "dappco.re/go/core/log" + "forge.lthn.ai/core/gui/pkg/marketplace" + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +type MarketplaceListInput struct { + URL string `json:"url,omitempty"` +} + +type MarketplaceListOutput struct { + RegistryURL string `json:"registry_url"` + Manifests []marketplace.Manifest `json:"manifests"` +} + +func (s *Subsystem) marketplaceList(_ context.Context, _ *mcp.CallToolRequest, input MarketplaceListInput) (*mcp.CallToolResult, MarketplaceListOutput, error) { + r := s.core.Action("display.marketplace.list").Run(context.Background(), core.NewOptions( + core.Option{Key: "url", Value: input.URL}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, MarketplaceListOutput{}, e + } + return nil, MarketplaceListOutput{}, coreerr.E("mcp.marketplaceList", "display.marketplace.list failed", nil) + } + payload, ok := r.Value.(map[string]any) + if !ok { + return nil, MarketplaceListOutput{}, coreerr.E("mcp.marketplaceList", "unexpected result type", nil) + } + output := MarketplaceListOutput{RegistryURL: stringValue(payload, "registry_url")} + if manifests, ok := payload["manifests"].([]marketplace.Manifest); ok { + output.Manifests = manifests + } + return nil, output, nil +} + +type MarketplaceFetchInput struct { + URL string `json:"url"` +} + +func (s *Subsystem) marketplaceFetch(_ context.Context, _ *mcp.CallToolRequest, input MarketplaceFetchInput) (*mcp.CallToolResult, marketplace.Manifest, error) { + r := s.core.Action("display.marketplace.fetch").Run(context.Background(), core.NewOptions( + core.Option{Key: "url", Value: input.URL}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, marketplace.Manifest{}, e + } + return nil, marketplace.Manifest{}, coreerr.E("mcp.marketplaceFetch", "display.marketplace.fetch failed", nil) + } + manifest, ok := r.Value.(marketplace.Manifest) + if !ok { + return nil, marketplace.Manifest{}, coreerr.E("mcp.marketplaceFetch", "unexpected result type", nil) + } + return nil, manifest, nil +} + +type MarketplaceVerifyInput struct { + URL string `json:"url"` +} + +type MarketplaceVerifyOutput struct { + Manifest marketplace.Manifest `json:"manifest"` + Digest string `json:"digest"` +} + +func (s *Subsystem) marketplaceVerify(_ context.Context, _ *mcp.CallToolRequest, input MarketplaceVerifyInput) (*mcp.CallToolResult, MarketplaceVerifyOutput, error) { + r := s.core.Action("display.marketplace.verify").Run(context.Background(), core.NewOptions( + core.Option{Key: "url", Value: input.URL}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, MarketplaceVerifyOutput{}, e + } + return nil, MarketplaceVerifyOutput{}, coreerr.E("mcp.marketplaceVerify", "display.marketplace.verify failed", nil) + } + payload, ok := r.Value.(map[string]any) + if !ok { + return nil, MarketplaceVerifyOutput{}, coreerr.E("mcp.marketplaceVerify", "unexpected result type", nil) + } + output := MarketplaceVerifyOutput{Digest: stringValue(payload, "digest")} + if manifest, ok := payload["manifest"].(marketplace.Manifest); ok { + output.Manifest = manifest + } + return nil, output, nil +} + +type MarketplaceInstallInput struct { + URL string `json:"url"` + InstallDir string `json:"install_dir,omitempty"` + GitBinary string `json:"git_binary,omitempty"` +} + +type MarketplaceInstallOutput struct { + Manifest marketplace.Manifest `json:"manifest"` + Digest string `json:"digest"` + TargetDir string `json:"target_dir"` + InstallDir string `json:"install_dir"` +} + +func (s *Subsystem) marketplaceInstall(_ context.Context, _ *mcp.CallToolRequest, input MarketplaceInstallInput) (*mcp.CallToolResult, MarketplaceInstallOutput, error) { + r := s.core.Action("display.marketplace.install").Run(context.Background(), core.NewOptions( + core.Option{Key: "url", Value: input.URL}, + core.Option{Key: "install_dir", Value: input.InstallDir}, + core.Option{Key: "git_binary", Value: input.GitBinary}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, MarketplaceInstallOutput{}, e + } + return nil, MarketplaceInstallOutput{}, coreerr.E("mcp.marketplaceInstall", "display.marketplace.install failed", nil) + } + payload, ok := r.Value.(map[string]any) + if !ok { + return nil, MarketplaceInstallOutput{}, coreerr.E("mcp.marketplaceInstall", "unexpected result type", nil) + } + output := MarketplaceInstallOutput{ + Digest: stringValue(payload, "digest"), + TargetDir: stringValue(payload, "target_dir"), + InstallDir: stringValue(payload, "install_dir"), + } + if manifest, ok := payload["manifest"].(marketplace.Manifest); ok { + output.Manifest = manifest + } + return nil, output, nil +} + +func (s *Subsystem) registerMarketplaceTools(server *mcp.Server) { + addTool(s, server, &mcp.Tool{ + Name: "marketplace_list", + Description: `List marketplace manifests from a registry. Example: {"url":"https://example.com/marketplace.yaml"}`, + }, s.marketplaceList) + addTool(s, server, &mcp.Tool{ + Name: "marketplace_fetch", + Description: `Fetch a marketplace manifest without installing it. Example: {"url":"https://example.com/core-ui.yaml"}`, + }, s.marketplaceFetch) + addTool(s, server, &mcp.Tool{ + Name: "marketplace_verify", + Description: `Fetch and verify a signed marketplace manifest. Example: {"url":"https://example.com/core-ui.yaml"}`, + }, s.marketplaceVerify) + addTool(s, server, &mcp.Tool{ + Name: "marketplace_install", + Description: `Fetch, verify, and install a marketplace manifest. Example: {"url":"https://example.com/core-ui.yaml","install_dir":"/Users/me/.core/apps"}`, + }, s.marketplaceInstall) +}