mcp-framework/manifest/manifest.go

311 lines
8.6 KiB
Go

package manifest
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/BurntSushi/toml"
"forge.lclr.dev/AI/mcp-framework/update"
)
const DefaultFile = "mcp.toml"
const EmbeddedSource = "embedded:mcp.toml"
type File struct {
BinaryName string `toml:"binary_name"`
DocsURL string `toml:"docs_url"`
Update Update `toml:"update"`
Environment Environment `toml:"environment"`
SecretStore SecretStore `toml:"secret_store"`
Profiles Profiles `toml:"profiles"`
Bootstrap Bootstrap `toml:"bootstrap"`
Config Config `toml:"config"`
}
type Update struct {
SourceName string `toml:"source_name"`
Driver string `toml:"driver"`
Repository string `toml:"repository"`
BaseURL string `toml:"base_url"`
LatestReleaseURL string `toml:"latest_release_url"`
AssetNameTemplate string `toml:"asset_name_template"`
ChecksumAssetName string `toml:"checksum_asset_name"`
ChecksumRequired bool `toml:"checksum_required"`
SignatureAssetName string `toml:"signature_asset_name"`
SignatureRequired bool `toml:"signature_required"`
SignaturePublicKey string `toml:"signature_public_key"`
SignaturePublicKeyEnvNames []string `toml:"signature_public_key_env_names"`
TokenHeader string `toml:"token_header"`
TokenPrefix string `toml:"token_prefix"`
TokenEnvNames []string `toml:"token_env_names"`
}
type Environment struct {
Known []string `toml:"known"`
}
type SecretStore struct {
BackendPolicy string `toml:"backend_policy"`
BitwardenCache *bool `toml:"bitwarden_cache"`
}
type Profiles struct {
Default string `toml:"default"`
Known []string `toml:"known"`
}
type Bootstrap struct {
Description string `toml:"description"`
}
type Config struct {
Fields []ConfigField `toml:"fields"`
}
type ConfigField struct {
Name string `toml:"name"`
Flag string `toml:"flag"`
Env string `toml:"env"`
ConfigKey string `toml:"config_key"`
SecretKeyTemplate string `toml:"secret_key_template"`
Type string `toml:"type"`
Label string `toml:"label"`
Default string `toml:"default"`
Required bool `toml:"required"`
Sources []string `toml:"sources"`
}
type BootstrapMetadata struct {
BinaryName string
Description string
DocsURL string
DefaultProfile string
Profiles []string
}
type ScaffoldMetadata struct {
BinaryName string
DocsURL string
KnownEnvironmentVariables []string
SecretStorePolicy string
DefaultProfile string
Profiles []string
}
func Find(startDir string) (string, error) {
dir := strings.TrimSpace(startDir)
if dir == "" {
wd, err := os.Getwd()
if err != nil {
return "", fmt.Errorf("resolve working directory: %w", err)
}
dir = wd
}
absDir, err := filepath.Abs(dir)
if err != nil {
return "", fmt.Errorf("resolve manifest search path %q: %w", dir, err)
}
for {
path := filepath.Join(absDir, DefaultFile)
info, err := os.Stat(path)
switch {
case err == nil:
if info.IsDir() {
return "", fmt.Errorf("manifest path %q is a directory", path)
}
return path, nil
case !errors.Is(err, os.ErrNotExist):
return "", fmt.Errorf("stat manifest %q: %w", path, err)
}
parent := filepath.Dir(absDir)
if parent == absDir {
return "", fmt.Errorf("find manifest %q from %q: %w", DefaultFile, dir, os.ErrNotExist)
}
absDir = parent
}
}
func Load(path string) (File, error) {
data, err := os.ReadFile(path)
if err != nil {
return File{}, fmt.Errorf("read manifest %s: %w", path, err)
}
return parse(data, path)
}
func LoadEmbedded(content string) (File, string, error) {
trimmed := strings.TrimSpace(content)
if trimmed == "" {
return File{}, "", os.ErrNotExist
}
file, err := parse([]byte(trimmed), EmbeddedSource)
if err != nil {
return File{}, "", err
}
return file, EmbeddedSource, nil
}
func LoadDefaultOrEmbedded(startDir, embeddedContent string) (File, string, error) {
file, path, err := LoadDefault(startDir)
if err == nil {
return file, path, nil
}
if !errors.Is(err, os.ErrNotExist) {
return File{}, "", err
}
return LoadEmbedded(embeddedContent)
}
func parse(data []byte, source string) (File, error) {
var file File
if err := toml.Unmarshal(data, &file); err != nil {
return File{}, fmt.Errorf("parse manifest %s: %w", source, err)
}
file.normalize()
return file, nil
}
func LoadDefault(startDir string) (File, string, error) {
path, err := Find(startDir)
if err != nil {
return File{}, "", err
}
file, err := Load(path)
if err != nil {
return File{}, "", err
}
return file, path, nil
}
func (f *File) normalize() {
f.BinaryName = strings.TrimSpace(f.BinaryName)
f.DocsURL = strings.TrimSpace(f.DocsURL)
f.Update.normalize()
f.Environment.normalize()
f.SecretStore.normalize()
f.Profiles.normalize()
f.Bootstrap.normalize()
f.Config.normalize()
}
func (u *Update) normalize() {
u.SourceName = strings.TrimSpace(u.SourceName)
u.Driver = strings.ToLower(strings.TrimSpace(u.Driver))
u.Repository = strings.Trim(strings.TrimSpace(u.Repository), "/")
u.BaseURL = strings.TrimRight(strings.TrimSpace(u.BaseURL), "/")
u.LatestReleaseURL = strings.TrimSpace(u.LatestReleaseURL)
u.AssetNameTemplate = strings.TrimSpace(u.AssetNameTemplate)
u.ChecksumAssetName = strings.TrimSpace(u.ChecksumAssetName)
u.SignatureAssetName = strings.TrimSpace(u.SignatureAssetName)
u.SignaturePublicKey = strings.TrimSpace(u.SignaturePublicKey)
u.SignaturePublicKeyEnvNames = normalizeStringList(u.SignaturePublicKeyEnvNames)
u.TokenHeader = strings.TrimSpace(u.TokenHeader)
u.TokenPrefix = strings.TrimSpace(u.TokenPrefix)
u.TokenEnvNames = normalizeStringList(u.TokenEnvNames)
}
func (e *Environment) normalize() {
e.Known = normalizeStringList(e.Known)
}
func (s *SecretStore) normalize() {
s.BackendPolicy = strings.TrimSpace(s.BackendPolicy)
}
func (p *Profiles) normalize() {
p.Default = strings.TrimSpace(p.Default)
p.Known = normalizeStringList(p.Known)
}
func (b *Bootstrap) normalize() {
b.Description = strings.TrimSpace(b.Description)
}
func (c *Config) normalize() {
for i := range c.Fields {
c.Fields[i].normalize()
}
}
func (f *ConfigField) normalize() {
f.Name = strings.TrimSpace(f.Name)
f.Flag = strings.TrimSpace(f.Flag)
f.Env = strings.TrimSpace(f.Env)
f.ConfigKey = strings.TrimSpace(f.ConfigKey)
f.SecretKeyTemplate = strings.TrimSpace(f.SecretKeyTemplate)
f.Type = strings.ToLower(strings.TrimSpace(f.Type))
f.Label = strings.TrimSpace(f.Label)
f.Default = strings.TrimSpace(f.Default)
f.Sources = normalizeStringList(f.Sources)
}
func (u Update) ReleaseSource() update.ReleaseSource {
u.normalize()
return update.ReleaseSource{
Name: u.SourceName,
Driver: u.Driver,
Repository: u.Repository,
BaseURL: u.BaseURL,
LatestReleaseURL: u.LatestReleaseURL,
AssetNameTemplate: u.AssetNameTemplate,
ChecksumAssetName: u.ChecksumAssetName,
ChecksumRequired: u.ChecksumRequired,
SignatureAssetName: u.SignatureAssetName,
SignatureRequired: u.SignatureRequired,
SignaturePublicKey: u.SignaturePublicKey,
SignaturePublicKeyEnvNames: append([]string(nil), u.SignaturePublicKeyEnvNames...),
TokenHeader: u.TokenHeader,
TokenPrefix: u.TokenPrefix,
TokenEnvNames: append([]string(nil), u.TokenEnvNames...),
}
}
func (f File) BootstrapInfo() BootstrapMetadata {
f.normalize()
return BootstrapMetadata{
BinaryName: f.BinaryName,
Description: f.Bootstrap.Description,
DocsURL: f.DocsURL,
DefaultProfile: f.Profiles.Default,
Profiles: append([]string(nil), f.Profiles.Known...),
}
}
func (f File) ScaffoldInfo() ScaffoldMetadata {
f.normalize()
return ScaffoldMetadata{
BinaryName: f.BinaryName,
DocsURL: f.DocsURL,
KnownEnvironmentVariables: append([]string(nil), f.Environment.Known...),
SecretStorePolicy: f.SecretStore.BackendPolicy,
DefaultProfile: f.Profiles.Default,
Profiles: append([]string(nil), f.Profiles.Known...),
}
}
func normalizeStringList(values []string) []string {
normalized := values[:0]
for _, value := range values {
if trimmed := strings.TrimSpace(value); trimmed != "" {
normalized = append(normalized, trimmed)
}
}
return normalized
}