package generate import ( "bytes" "errors" "fmt" "go/format" "go/token" "os" "path/filepath" "sort" "strconv" "strings" "gitea.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 } 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, }, } 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 "gitea.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 "gitea.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 "gitea.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 "gitea.lclr.dev/AI/mcp-framework/secretstore" ) type SecretStoreOptions struct { ServiceName string LookupEnv func(string) (string, bool) KWalletAppID string KWalletFolder string BitwardenCommand string BitwardenDebug 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, 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, 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 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 }