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:
parent
5af6f9a576
commit
1823893c16
13 changed files with 646 additions and 0 deletions
14
go.mod
14
go.mod
|
|
@ -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
39
go.sum
Normal 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
89
mcp/core_cli.go
Normal 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
35
mcp/core_cli_test.go
Normal 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
37
mcp/ethics.go
Normal 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
37
mcp/ethics_test.go
Normal 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
14
mcp/main.go
Normal 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
33
mcp/marketplace.go
Normal 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
52
mcp/marketplace_test.go
Normal 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
120
mcp/plugin_info.go
Normal 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
77
mcp/server.go
Normal 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
43
mcp/types.go
Normal 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
56
mcp/util.go
Normal 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
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue