feat(mcp): add marketplace server

Summary:\n- added vendor-neutral MCP stdio server with marketplace, core CLI, and ethics tools\n- implemented plugin discovery across commands and skills\n- added Good/Bad/Ugly tests and Go module dependency updates
This commit is contained in:
Snider 2026-02-05 20:40:06 +00:00
parent 5af6f9a576
commit 1823893c16
13 changed files with 646 additions and 0 deletions

14
go.mod
View file

@ -1,3 +1,17 @@
module core-agent
go 1.24
require github.com/mark3labs/mcp-go v0.43.2
require (
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/invopop/jsonschema v0.13.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

39
go.sum Normal file
View file

@ -0,0 +1,39 @@
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mark3labs/mcp-go v0.43.2 h1:21PUSlWWiSbUPQwXIJ5WKlETixpFpq+WBpbMGDSVy/I=
github.com/mark3labs/mcp-go v0.43.2/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

89
mcp/core_cli.go Normal file
View file

@ -0,0 +1,89 @@
package main
import (
"bytes"
"context"
"errors"
"fmt"
"os/exec"
"strings"
"time"
"github.com/mark3labs/mcp-go/mcp"
)
var allowedCorePrefixes = map[string]struct{}{
"dev": {},
"go": {},
"php": {},
"build": {},
}
func coreCliHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
command, err := request.RequireString("command")
if err != nil {
return mcp.NewToolResultError("command is required"), nil
}
args := request.GetStringSlice("args", nil)
base, mergedArgs, err := normalizeCoreCommand(command, args)
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
execCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
result := runCoreCommand(execCtx, base, mergedArgs)
return mcp.NewToolResultStructuredOnly(result), nil
}
func normalizeCoreCommand(command string, args []string) (string, []string, error) {
parts := strings.Fields(command)
if len(parts) == 0 {
return "", nil, errors.New("command cannot be empty")
}
base := parts[0]
if _, ok := allowedCorePrefixes[base]; !ok {
return "", nil, fmt.Errorf("command not allowed: %s", base)
}
merged := append([]string{}, parts[1:]...)
merged = append(merged, args...)
return base, merged, nil
}
func runCoreCommand(ctx context.Context, command string, args []string) CoreCliResult {
cmd := exec.CommandContext(ctx, "core", append([]string{command}, args...)...)
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
exitCode := 0
if err := cmd.Run(); err != nil {
exitCode = 1
if exitErr, ok := err.(*exec.ExitError); ok {
exitCode = exitErr.ExitCode()
} else if errors.Is(err, context.DeadlineExceeded) {
exitCode = 124
}
if errors.Is(err, context.DeadlineExceeded) {
if stderr.Len() > 0 {
stderr.WriteString("\n")
}
stderr.WriteString("command timed out after 30s")
}
}
return CoreCliResult{
Command: command,
Args: args,
Stdout: stdout.String(),
Stderr: stderr.String(),
ExitCode: exitCode,
}
}

35
mcp/core_cli_test.go Normal file
View file

@ -0,0 +1,35 @@
package main
import "testing"
func TestNormalizeCoreCommand_Good(t *testing.T) {
command, args, err := normalizeCoreCommand("go", []string{"test"})
if err != nil {
t.Fatalf("expected command to be allowed: %v", err)
}
if command != "go" {
t.Fatalf("expected go command, got %s", command)
}
if len(args) != 1 || args[0] != "test" {
t.Fatalf("unexpected args: %#v", args)
}
}
func TestNormalizeCoreCommand_Bad(t *testing.T) {
if _, _, err := normalizeCoreCommand("rm -rf", nil); err == nil {
t.Fatalf("expected command to be rejected")
}
}
func TestNormalizeCoreCommand_Ugly(t *testing.T) {
command, args, err := normalizeCoreCommand("go test", []string{"-v"})
if err != nil {
t.Fatalf("expected command to be allowed: %v", err)
}
if command != "go" {
t.Fatalf("expected go command, got %s", command)
}
if len(args) != 2 || args[0] != "test" || args[1] != "-v" {
t.Fatalf("unexpected args: %#v", args)
}
}

37
mcp/ethics.go Normal file
View file

@ -0,0 +1,37 @@
package main
import (
"context"
"fmt"
"os"
"path/filepath"
"github.com/mark3labs/mcp-go/mcp"
)
const ethicsModalPath = "codex/ethics/MODAL.md"
const ethicsAxiomsPath = "codex/ethics/kernel/axioms.json"
func ethicsCheckHandler(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
root, err := findRepoRoot()
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("failed to locate repo root: %v", err)), nil
}
modalBytes, err := os.ReadFile(filepath.Join(root, ethicsModalPath))
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("failed to read modal: %v", err)), nil
}
axioms, err := readJSONMap(filepath.Join(root, ethicsAxiomsPath))
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("failed to read axioms: %v", err)), nil
}
payload := EthicsContext{
Modal: string(modalBytes),
Axioms: axioms,
}
return mcp.NewToolResultStructuredOnly(payload), nil
}

37
mcp/ethics_test.go Normal file
View file

@ -0,0 +1,37 @@
package main
import (
"os"
"path/filepath"
"testing"
)
func TestEthicsCheck_Good(t *testing.T) {
root, err := findRepoRoot()
if err != nil {
t.Fatalf("expected repo root: %v", err)
}
modalPath := filepath.Join(root, ethicsModalPath)
modal, err := os.ReadFile(modalPath)
if err != nil {
t.Fatalf("expected modal to read: %v", err)
}
if len(modal) == 0 {
t.Fatalf("expected modal content")
}
axioms, err := readJSONMap(filepath.Join(root, ethicsAxiomsPath))
if err != nil {
t.Fatalf("expected axioms to read: %v", err)
}
if len(axioms) == 0 {
t.Fatalf("expected axioms data")
}
}
func TestReadJSONMap_Bad(t *testing.T) {
if _, err := readJSONMap("/missing/file.json"); err == nil {
t.Fatalf("expected error for missing json")
}
}

14
mcp/main.go Normal file
View file

@ -0,0 +1,14 @@
package main
import (
"log"
"github.com/mark3labs/mcp-go/server"
)
func main() {
srv := newServer()
if err := server.ServeStdio(srv); err != nil {
log.Fatalf("mcp server failed: %v", err)
}
}

33
mcp/marketplace.go Normal file
View file

@ -0,0 +1,33 @@
package main
import (
"context"
"fmt"
"path/filepath"
"github.com/mark3labs/mcp-go/mcp"
)
func loadMarketplace() (Marketplace, string, error) {
root, err := findRepoRoot()
if err != nil {
return Marketplace{}, "", err
}
path := filepath.Join(root, marketplacePath)
var marketplace Marketplace
if err := readJSONFile(path, &marketplace); err != nil {
return Marketplace{}, "", err
}
return marketplace, root, nil
}
func marketplaceListHandler(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
marketplace, _, err := loadMarketplace()
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("failed to load marketplace: %v", err)), nil
}
return mcp.NewToolResultStructuredOnly(marketplace), nil
}

52
mcp/marketplace_test.go Normal file
View file

@ -0,0 +1,52 @@
package main
import (
"path/filepath"
"testing"
)
func TestMarketplaceLoad_Good(t *testing.T) {
marketplace, root, err := loadMarketplace()
if err != nil {
t.Fatalf("expected marketplace to load: %v", err)
}
if marketplace.Name == "" {
t.Fatalf("expected marketplace name to be set")
}
if len(marketplace.Plugins) == 0 {
t.Fatalf("expected marketplace plugins")
}
if root == "" {
t.Fatalf("expected repo root")
}
}
func TestMarketplacePluginInfo_Bad(t *testing.T) {
marketplace, _, err := loadMarketplace()
if err != nil {
t.Fatalf("expected marketplace to load: %v", err)
}
if _, ok := findMarketplacePlugin(marketplace, "missing-plugin"); ok {
t.Fatalf("expected missing plugin")
}
}
func TestMarketplacePluginInfo_Good(t *testing.T) {
marketplace, root, err := loadMarketplace()
if err != nil {
t.Fatalf("expected marketplace to load: %v", err)
}
plugin, ok := findMarketplacePlugin(marketplace, "claude-code")
if !ok {
t.Fatalf("expected claude-code plugin")
}
commands, err := listCommands(filepath.Join(root, plugin.Source))
if err != nil {
t.Fatalf("expected commands to list: %v", err)
}
if len(commands) == 0 {
t.Fatalf("expected commands for claude-code")
}
}

120
mcp/plugin_info.go Normal file
View file

@ -0,0 +1,120 @@
package main
import (
"context"
"fmt"
"os"
"path/filepath"
"sort"
"github.com/mark3labs/mcp-go/mcp"
)
func marketplacePluginInfoHandler(_ context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
name, err := request.RequireString("name")
if err != nil {
return mcp.NewToolResultError("name is required"), nil
}
marketplace, root, err := loadMarketplace()
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("failed to load marketplace: %v", err)), nil
}
plugin, ok := findMarketplacePlugin(marketplace, name)
if !ok {
return mcp.NewToolResultError(fmt.Sprintf("plugin not found: %s", name)), nil
}
path := filepath.Join(root, plugin.Source)
commands, _ := listCommands(path)
skills, _ := listSkills(path)
manifest, _ := loadPluginManifest(path)
info := PluginInfo{
Plugin: plugin,
Path: path,
Manifest: manifest,
Commands: commands,
Skills: skills,
}
return mcp.NewToolResultStructuredOnly(info), nil
}
func findMarketplacePlugin(marketplace Marketplace, name string) (MarketplacePlugin, bool) {
for _, plugin := range marketplace.Plugins {
if plugin.Name == name {
return plugin, true
}
}
return MarketplacePlugin{}, false
}
func listCommands(path string) ([]string, error) {
commandsPath := filepath.Join(path, "commands")
info, err := os.Stat(commandsPath)
if err != nil || !info.IsDir() {
return nil, nil
}
var commands []string
_ = filepath.WalkDir(commandsPath, func(entryPath string, entry os.DirEntry, err error) error {
if err != nil {
return nil
}
if entry.IsDir() {
return nil
}
rel, relErr := filepath.Rel(commandsPath, entryPath)
if relErr != nil {
return nil
}
commands = append(commands, filepath.ToSlash(rel))
return nil
})
sort.Strings(commands)
return commands, nil
}
func listSkills(path string) ([]string, error) {
skillsPath := filepath.Join(path, "skills")
info, err := os.Stat(skillsPath)
if err != nil || !info.IsDir() {
return nil, nil
}
entries, err := os.ReadDir(skillsPath)
if err != nil {
return nil, err
}
var skills []string
for _, entry := range entries {
if entry.IsDir() {
skills = append(skills, entry.Name())
}
}
sort.Strings(skills)
return skills, nil
}
func loadPluginManifest(path string) (map[string]any, error) {
candidates := []string{
filepath.Join(path, ".claude-plugin", "plugin.json"),
filepath.Join(path, ".codex-plugin", "plugin.json"),
filepath.Join(path, "gemini-extension.json"),
}
for _, candidate := range candidates {
payload, err := readJSONMap(candidate)
if err == nil {
return payload, nil
}
}
return nil, nil
}

77
mcp/server.go Normal file
View file

@ -0,0 +1,77 @@
package main
import (
"encoding/json"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
const serverName = "host-uk-marketplace"
const serverVersion = "0.1.0"
func newServer() *server.MCPServer {
srv := server.NewMCPServer(
serverName,
serverVersion,
)
srv.AddTool(marketplaceListTool(), marketplaceListHandler)
srv.AddTool(marketplacePluginInfoTool(), marketplacePluginInfoHandler)
srv.AddTool(coreCliTool(), coreCliHandler)
srv.AddTool(ethicsCheckTool(), ethicsCheckHandler)
return srv
}
func marketplaceListTool() mcp.Tool {
return mcp.NewTool(
"marketplace_list",
mcp.WithDescription("List available marketplace plugins"),
)
}
func marketplacePluginInfoTool() mcp.Tool {
return mcp.NewTool(
"marketplace_plugin_info",
mcp.WithDescription("Return plugin metadata, commands, and skills"),
mcp.WithString("name", mcp.Required(), mcp.Description("Marketplace plugin name")),
)
}
func coreCliTool() mcp.Tool {
rawSchema, err := json.Marshal(map[string]any{
"type": "object",
"properties": map[string]any{
"command": map[string]any{
"type": "string",
"description": "Core CLI command group (dev, go, php, build)",
},
"args": map[string]any{
"type": "array",
"items": map[string]any{"type": "string"},
"description": "Arguments for the command",
},
},
"required": []string{"command"},
})
options := []mcp.ToolOption{
mcp.WithDescription("Run approved core CLI commands"),
}
if err == nil {
options = append(options, mcp.WithRawInputSchema(rawSchema))
}
return mcp.NewTool(
"core_cli",
options...,
)
}
func ethicsCheckTool() mcp.Tool {
return mcp.NewTool(
"ethics_check",
mcp.WithDescription("Return the Axioms of Life ethics modal and kernel"),
)
}

43
mcp/types.go Normal file
View file

@ -0,0 +1,43 @@
package main
type Marketplace struct {
Schema string `json:"$schema,omitempty"`
Name string `json:"name"`
Description string `json:"description"`
Owner MarketplaceOwner `json:"owner"`
Plugins []MarketplacePlugin `json:"plugins"`
}
type MarketplaceOwner struct {
Name string `json:"name"`
Email string `json:"email"`
}
type MarketplacePlugin struct {
Name string `json:"name"`
Description string `json:"description"`
Version string `json:"version"`
Source string `json:"source"`
Category string `json:"category"`
}
type PluginInfo struct {
Plugin MarketplacePlugin `json:"plugin"`
Path string `json:"path"`
Manifest map[string]any `json:"manifest,omitempty"`
Commands []string `json:"commands,omitempty"`
Skills []string `json:"skills,omitempty"`
}
type CoreCliResult struct {
Command string `json:"command"`
Args []string `json:"args"`
Stdout string `json:"stdout"`
Stderr string `json:"stderr"`
ExitCode int `json:"exit_code"`
}
type EthicsContext struct {
Modal string `json:"modal"`
Axioms map[string]any `json:"axioms"`
}

56
mcp/util.go Normal file
View file

@ -0,0 +1,56 @@
package main
import (
"encoding/json"
"errors"
"os"
"path/filepath"
)
const marketplacePath = ".claude-plugin/marketplace.json"
func findRepoRoot() (string, error) {
cwd, err := os.Getwd()
if err != nil {
return "", err
}
path := cwd
for {
candidate := filepath.Join(path, marketplacePath)
if _, err := os.Stat(candidate); err == nil {
return path, nil
}
parent := filepath.Dir(path)
if parent == path {
break
}
path = parent
}
return "", errors.New("repository root not found")
}
func readJSONFile(path string, target any) error {
data, err := os.ReadFile(path)
if err != nil {
return err
}
return json.Unmarshal(data, target)
}
func readJSONMap(path string) (map[string]any, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var payload map[string]any
if err := json.Unmarshal(data, &payload); err != nil {
return nil, err
}
return payload, nil
}