package generate import ( "bytes" "errors" "fmt" "go/format" "go/token" "os" "path/filepath" "sort" "strconv" "strings" "forge.lclr.dev/AI/mcp-framework/manifest" ) var ErrGeneratedFilesOutdated = errors.New("generated files are not up to date") type Options struct { ProjectDir string ManifestPath string PackageDir string PackageName string Check bool } type Result struct { Root string Files []string } func Generate(options Options) (Result, error) { normalized, err := normalizeOptions(options) if err != nil { return Result{}, err } manifestFile, err := manifest.Load(normalized.ManifestPath) if err != nil { return Result{}, err } manifestContent, err := os.ReadFile(normalized.ManifestPath) if err != nil { return Result{}, fmt.Errorf("read manifest %s: %w", normalized.ManifestPath, err) } manifestLoader, err := renderManifestLoader(normalized.PackageName, string(manifestContent)) if err != nil { return Result{}, err } metadata, err := renderMetadata(normalized.PackageName, manifestFile) if err != nil { return Result{}, err } update, err := renderUpdate(normalized.PackageName) if err != nil { return Result{}, err } secretstore, err := renderSecretStore(normalized.PackageName) if err != nil { return Result{}, err } config, err := renderConfig(normalized.PackageName, manifestFile.Config.Fields) if err != nil { return Result{}, err } files := []generatedFile{ { Path: filepath.Join(normalized.PackageDir, "manifest.go"), Content: manifestLoader, Mode: 0o644, }, { Path: filepath.Join(normalized.PackageDir, "metadata.go"), Content: metadata, Mode: 0o644, }, { Path: filepath.Join(normalized.PackageDir, "update.go"), Content: update, Mode: 0o644, }, { Path: filepath.Join(normalized.PackageDir, "secretstore.go"), Content: secretstore, Mode: 0o644, }, } if strings.TrimSpace(config) != "" { files = append(files, generatedFile{ Path: filepath.Join(normalized.PackageDir, "config.go"), Content: config, Mode: 0o644, }) } written := make([]string, 0, len(files)) for _, file := range files { target := filepath.Join(normalized.ProjectDir, file.Path) if normalized.Check { current, err := os.ReadFile(target) if err != nil { if errors.Is(err, os.ErrNotExist) { return Result{}, fmt.Errorf("%w: %s", ErrGeneratedFilesOutdated, file.Path) } return Result{}, fmt.Errorf("read generated file %q: %w", target, err) } if !bytes.Equal(current, []byte(file.Content)) { return Result{}, fmt.Errorf("%w: %s", ErrGeneratedFilesOutdated, file.Path) } written = append(written, file.Path) continue } if err := writeGeneratedFile(target, file.Content, file.Mode); err != nil { return Result{}, err } written = append(written, file.Path) } sort.Strings(written) return Result{ Root: normalized.ProjectDir, Files: written, }, nil } type normalizedOptions struct { ProjectDir string ManifestPath string PackageDir string PackageName string Check bool } type generatedFile struct { Path string Content string Mode os.FileMode } func normalizeOptions(options Options) (normalizedOptions, error) { manifestPath := strings.TrimSpace(options.ManifestPath) projectDir := strings.TrimSpace(options.ProjectDir) if manifestPath == "" { baseDir := projectDir if baseDir == "" { wd, err := os.Getwd() if err != nil { return normalizedOptions{}, fmt.Errorf("resolve working directory: %w", err) } baseDir = wd } manifestPath = filepath.Join(baseDir, manifest.DefaultFile) } else if !filepath.IsAbs(manifestPath) { baseDir := projectDir if baseDir == "" { wd, err := os.Getwd() if err != nil { return normalizedOptions{}, fmt.Errorf("resolve working directory: %w", err) } baseDir = wd } manifestPath = filepath.Join(baseDir, manifestPath) } resolvedManifest, err := filepath.Abs(manifestPath) if err != nil { return normalizedOptions{}, fmt.Errorf("resolve manifest path %q: %w", manifestPath, err) } if projectDir == "" { projectDir = filepath.Dir(resolvedManifest) } resolvedProjectDir, err := filepath.Abs(projectDir) if err != nil { return normalizedOptions{}, fmt.Errorf("resolve project dir %q: %w", projectDir, err) } packageDir := filepath.Clean(strings.TrimSpace(options.PackageDir)) if packageDir == "." || packageDir == "" { packageDir = "mcpgen" } if filepath.IsAbs(packageDir) || packageDir == ".." || strings.HasPrefix(packageDir, ".."+string(filepath.Separator)) { return normalizedOptions{}, fmt.Errorf("package dir %q must be relative to the project", options.PackageDir) } packageName := strings.TrimSpace(options.PackageName) if packageName == "" { packageName = filepath.Base(packageDir) } if !token.IsIdentifier(packageName) { return normalizedOptions{}, fmt.Errorf("package name %q is not a valid Go identifier", packageName) } return normalizedOptions{ ProjectDir: resolvedProjectDir, ManifestPath: resolvedManifest, PackageDir: packageDir, PackageName: packageName, Check: options.Check, }, nil } func renderManifestLoader(packageName, manifestContent string) (string, error) { source := fmt.Sprintf(`// Code generated by mcp-framework generate. DO NOT EDIT. package %s import fwmanifest "forge.lclr.dev/AI/mcp-framework/manifest" const embeddedManifest = %s func LoadManifest(startDir string) (fwmanifest.File, string, error) { return fwmanifest.LoadDefaultOrEmbedded(startDir, embeddedManifest) } `, packageName, strconv.Quote(manifestContent)) formatted, err := format.Source([]byte(source)) if err != nil { return "", fmt.Errorf("format generated manifest loader: %w", err) } return string(formatted), nil } func renderMetadata(packageName string, manifestFile manifest.File) (string, error) { bootstrapInfo := manifestFile.BootstrapInfo() source := fmt.Sprintf(`// Code generated by mcp-framework generate. DO NOT EDIT. package %s import fwmanifest "forge.lclr.dev/AI/mcp-framework/manifest" const BinaryName = %s const DefaultDescription = %s const DocsURL = %s func BootstrapInfo(startDir string) (fwmanifest.BootstrapMetadata, string, error) { manifestFile, source, err := LoadManifest(startDir) if err != nil { return fwmanifest.BootstrapMetadata{}, "", err } return manifestFile.BootstrapInfo(), source, nil } func ScaffoldInfo(startDir string) (fwmanifest.ScaffoldMetadata, string, error) { manifestFile, source, err := LoadManifest(startDir) if err != nil { return fwmanifest.ScaffoldMetadata{}, "", err } return manifestFile.ScaffoldInfo(), source, nil } `, packageName, strconv.Quote(manifestFile.BinaryName), strconv.Quote(bootstrapInfo.Description), strconv.Quote(manifestFile.DocsURL)) return formatGenerated("metadata", source) } func renderUpdate(packageName string) (string, error) { source := fmt.Sprintf(`// Code generated by mcp-framework generate. DO NOT EDIT. package %s import ( "context" "flag" "fmt" "io" "strings" fwupdate "forge.lclr.dev/AI/mcp-framework/update" ) func UpdateOptions(version string, stdout io.Writer) (fwupdate.Options, error) { return UpdateOptionsFrom(".", version, stdout) } func UpdateOptionsFrom(startDir string, version string, stdout io.Writer) (fwupdate.Options, error) { manifestFile, _, err := LoadManifest(startDir) if err != nil { return fwupdate.Options{}, err } binaryName := strings.TrimSpace(manifestFile.BinaryName) if binaryName == "" { binaryName = BinaryName } return fwupdate.Options{ CurrentVersion: version, Stdout: stdout, BinaryName: binaryName, ReleaseSource: manifestFile.Update.ReleaseSource(), }, nil } func RunUpdate(ctx context.Context, args []string, version string, stdout io.Writer) error { return RunUpdateFrom(ctx, args, ".", version, stdout) } func RunUpdateFrom(ctx context.Context, args []string, startDir string, version string, stdout io.Writer) error { fs := flag.NewFlagSet("update", flag.ContinueOnError) fs.SetOutput(io.Discard) if err := fs.Parse(args); err != nil { return err } if fs.NArg() != 0 { return fmt.Errorf("update does not accept positional arguments: %%s", strings.Join(fs.Args(), ", ")) } options, err := UpdateOptionsFrom(startDir, version, stdout) if err != nil { return err } return fwupdate.Run(ctx, options) } `, packageName) return formatGenerated("update", source) } func renderSecretStore(packageName string) (string, error) { source := fmt.Sprintf(`// Code generated by mcp-framework generate. DO NOT EDIT. package %s import ( "os" "path/filepath" "strings" fwsecretstore "forge.lclr.dev/AI/mcp-framework/secretstore" ) type SecretStoreOptions struct { ServiceName string LookupEnv func(string) (string, bool) KWalletAppID string KWalletFolder string BitwardenCommand string BitwardenDebug bool DisableBitwardenCache bool Shell string ExecutableResolver fwsecretstore.ExecutableResolver } func OpenSecretStore(options SecretStoreOptions) (fwsecretstore.Store, error) { return fwsecretstore.OpenFromManifest(secretStoreOpenOptions(options)) } func DescribeSecretRuntime(options SecretStoreOptions) (fwsecretstore.RuntimeDescription, error) { return fwsecretstore.DescribeRuntime(secretStoreDescribeOptions(options)) } func PreflightSecretStore(options SecretStoreOptions) (fwsecretstore.PreflightReport, error) { return fwsecretstore.PreflightFromManifest(secretStoreDescribeOptions(options)) } func secretStoreOpenOptions(options SecretStoreOptions) fwsecretstore.OpenFromManifestOptions { return fwsecretstore.OpenFromManifestOptions{ ServiceName: secretStoreServiceName(options), LookupEnv: options.LookupEnv, KWalletAppID: options.KWalletAppID, KWalletFolder: options.KWalletFolder, BitwardenCommand: options.BitwardenCommand, BitwardenDebug: options.BitwardenDebug, DisableBitwardenCache: options.DisableBitwardenCache, Shell: options.Shell, ManifestLoader: LoadManifest, ExecutableResolver: options.ExecutableResolver, } } func secretStoreDescribeOptions(options SecretStoreOptions) fwsecretstore.DescribeRuntimeOptions { return fwsecretstore.DescribeRuntimeOptions{ ServiceName: secretStoreServiceName(options), LookupEnv: options.LookupEnv, KWalletAppID: options.KWalletAppID, KWalletFolder: options.KWalletFolder, BitwardenCommand: options.BitwardenCommand, BitwardenDebug: options.BitwardenDebug, DisableBitwardenCache: options.DisableBitwardenCache, Shell: options.Shell, ManifestLoader: LoadManifest, ExecutableResolver: options.ExecutableResolver, } } func secretStoreServiceName(options SecretStoreOptions) string { serviceName := strings.TrimSpace(options.ServiceName) if serviceName != "" { return serviceName } startDir := "." executableResolver := options.ExecutableResolver if executableResolver == nil { executableResolver = os.Executable } if executablePath, err := executableResolver(); err == nil { if dir := strings.TrimSpace(filepath.Dir(strings.TrimSpace(executablePath))); dir != "" { startDir = dir } } if manifestFile, _, err := LoadManifest(startDir); err == nil { if binaryName := strings.TrimSpace(manifestFile.BinaryName); binaryName != "" { return binaryName } } return BinaryName } `, packageName) return formatGenerated("secretstore", source) } func renderConfig(packageName string, fields []manifest.ConfigField) (string, error) { if len(fields) == 0 { return "", nil } var flagsBuilder strings.Builder var specsBuilder strings.Builder var setupBuilder strings.Builder for _, field := range fields { name := strings.TrimSpace(field.Name) if name == "" { return "", fmt.Errorf("generate config field: name must not be empty") } flagName := strings.TrimSpace(field.Flag) if flagName != "" { fmt.Fprintf( &flagsBuilder, "\tflags.values[%s] = fs.String(%s, \"\", %s)\n", strconv.Quote(name), strconv.Quote(flagName), strconv.Quote(configFieldLabel(field)), ) } fmt.Fprintf( &specsBuilder, "\t\t{Name: %s, Required: %t, DefaultValue: %s, Sources: []fwcli.ValueSource{%s}, FlagKey: %s, EnvKey: %s, ConfigKey: %s, SecretKey: replaceProfile(%s, profile)},\n", strconv.Quote(name), field.Required, strconv.Quote(field.Default), configSourceList(field.Sources), strconv.Quote(flagName), strconv.Quote(field.Env), strconv.Quote(field.ConfigKey), strconv.Quote(field.SecretKeyTemplate), ) fmt.Fprintf( &setupBuilder, "\t\t{Name: %s, Label: %s, Type: %s, Required: %t, Default: %s, ExistingSecret: existing[%s]},\n", strconv.Quote(name), strconv.Quote(configFieldLabel(field)), configSetupFieldType(field.Type), field.Required, strconv.Quote(field.Default), strconv.Quote(name), ) } source := fmt.Sprintf(`// Code generated by mcp-framework generate. DO NOT EDIT. package %s import ( "flag" "strings" fwcli "forge.lclr.dev/AI/mcp-framework/cli" ) type ConfigFlags struct { values map[string]*string } func AddConfigFlags(fs *flag.FlagSet) ConfigFlags { if fs == nil { fs = flag.CommandLine } flags := ConfigFlags{ values: make(map[string]*string), } %s return flags } func ConfigFlagValues(flags ConfigFlags) map[string]string { values := make(map[string]string) for name, value := range flags.values { if value == nil { continue } if trimmed := strings.TrimSpace(*value); trimmed != "" { values[name] = trimmed } } return values } func ResolveFieldSpecs(profile string) []fwcli.FieldSpec { return []fwcli.FieldSpec{ %s } } func SetupFields(existing map[string]string) []fwcli.SetupField { if existing == nil { existing = map[string]string{} } return []fwcli.SetupField{ %s } } func replaceProfile(value, profile string) string { return strings.ReplaceAll(value, "{profile}", strings.TrimSpace(profile)) } `, packageName, flagsBuilder.String(), specsBuilder.String(), setupBuilder.String()) return formatGenerated("config", source) } func configFieldLabel(field manifest.ConfigField) string { if label := strings.TrimSpace(field.Label); label != "" { return label } return strings.TrimSpace(field.Name) } func configSourceList(sources []string) string { if len(sources) == 0 { return "" } parts := make([]string, 0, len(sources)) for _, source := range sources { switch strings.TrimSpace(source) { case "flag": parts = append(parts, "fwcli.SourceFlag") case "env": parts = append(parts, "fwcli.SourceEnv") case "config": parts = append(parts, "fwcli.SourceConfig") case "secret": parts = append(parts, "fwcli.SourceSecret") } } return strings.Join(parts, ", ") } func configSetupFieldType(fieldType string) string { switch strings.TrimSpace(fieldType) { case "url": return "fwcli.SetupFieldURL" case "secret": return "fwcli.SetupFieldSecret" case "bool": return "fwcli.SetupFieldBool" case "list": return "fwcli.SetupFieldList" default: return "fwcli.SetupFieldString" } } func formatGenerated(name, source string) (string, error) { formatted, err := format.Source([]byte(source)) if err != nil { return "", fmt.Errorf("format generated %s: %w", name, err) } return string(formatted), nil } func writeGeneratedFile(path, content string, mode os.FileMode) error { current, err := os.ReadFile(path) if err == nil && bytes.Equal(current, []byte(content)) { return nil } if err != nil && !errors.Is(err, os.ErrNotExist) { return fmt.Errorf("read generated file %q: %w", path, err) } dir := filepath.Dir(path) if err := os.MkdirAll(dir, 0o755); err != nil { return fmt.Errorf("create generated directory %q: %w", dir, err) } if mode == 0 { mode = 0o644 } if err := os.WriteFile(path, []byte(content), mode); err != nil { return fmt.Errorf("write generated file %q: %w", path, err) } return nil }