Add marketplace display and MCP actions
Some checks are pending
Security Scan / security (push) Waiting to run
Test / test (push) Waiting to run

This commit is contained in:
Snider 2026-04-15 20:53:13 +01:00
parent 2b59d5892a
commit 710bd4f7b5
4 changed files with 284 additions and 0 deletions

View file

@ -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)

123
pkg/display/marketplace.go Normal file
View file

@ -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")
}

View file

@ -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)
}

View file

@ -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)
}