feat: generate manifest helper glue
This commit is contained in:
parent
20b5026f9d
commit
a79f73825f
3 changed files with 439 additions and 12 deletions
|
|
@ -1,8 +1,7 @@
|
||||||
# Génération depuis `mcp.toml`
|
# Génération depuis `mcp.toml`
|
||||||
|
|
||||||
La commande `mcp-framework generate` génère la glue Go dérivée du manifeste
|
La commande `mcp-framework generate` génère la glue Go dérivée du manifeste
|
||||||
racine d'un projet existant. Le premier usage couvert est le loader de
|
racine d'un projet existant.
|
||||||
manifeste embarqué.
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
|
@ -18,9 +17,12 @@ génère :
|
||||||
```text
|
```text
|
||||||
mcpgen/
|
mcpgen/
|
||||||
manifest.go
|
manifest.go
|
||||||
|
metadata.go
|
||||||
|
update.go
|
||||||
|
secretstore.go
|
||||||
```
|
```
|
||||||
|
|
||||||
Le fichier généré expose :
|
Le package généré expose le loader de manifeste :
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func LoadManifest(startDir string) (manifest.File, string, error)
|
func LoadManifest(startDir string) (manifest.File, string, error)
|
||||||
|
|
@ -30,6 +32,48 @@ Cette fonction appelle `manifest.LoadDefaultOrEmbedded`. En développement, un
|
||||||
`mcp.toml` présent sur disque reste prioritaire. Pour un binaire copié seul,
|
`mcp.toml` présent sur disque reste prioritaire. Pour un binaire copié seul,
|
||||||
elle utilise le contenu du manifeste embarqué au moment de la génération.
|
elle utilise le contenu du manifeste embarqué au moment de la génération.
|
||||||
|
|
||||||
|
Il expose aussi des helpers dérivés du manifeste :
|
||||||
|
|
||||||
|
```go
|
||||||
|
const BinaryName = "my-mcp"
|
||||||
|
const DefaultDescription = "..."
|
||||||
|
const DocsURL = "..."
|
||||||
|
|
||||||
|
func BootstrapInfo(startDir string) (manifest.BootstrapMetadata, string, error)
|
||||||
|
func ScaffoldInfo(startDir string) (manifest.ScaffoldMetadata, string, error)
|
||||||
|
```
|
||||||
|
|
||||||
|
Pour l'auto-update :
|
||||||
|
|
||||||
|
```go
|
||||||
|
func UpdateOptions(version string, stdout io.Writer) (update.Options, error)
|
||||||
|
func UpdateOptionsFrom(startDir string, version string, stdout io.Writer) (update.Options, error)
|
||||||
|
func RunUpdate(ctx context.Context, args []string, version string, stdout io.Writer) error
|
||||||
|
func RunUpdateFrom(ctx context.Context, args []string, startDir string, version string, stdout io.Writer) error
|
||||||
|
```
|
||||||
|
|
||||||
|
`RunUpdate` parse les flags de la commande `update`, refuse les arguments
|
||||||
|
positionnels, charge le manifeste via `LoadManifest`, puis appelle
|
||||||
|
`update.Run`.
|
||||||
|
|
||||||
|
Pour les secrets :
|
||||||
|
|
||||||
|
```go
|
||||||
|
type SecretStoreOptions struct {
|
||||||
|
ServiceName string
|
||||||
|
LookupEnv func(string) (string, bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
func OpenSecretStore(options SecretStoreOptions) (secretstore.Store, error)
|
||||||
|
func DescribeSecretRuntime(options SecretStoreOptions) (secretstore.RuntimeDescription, error)
|
||||||
|
func PreflightSecretStore(options SecretStoreOptions) (secretstore.PreflightReport, error)
|
||||||
|
```
|
||||||
|
|
||||||
|
`SecretStoreOptions` contient aussi les options techniques du package
|
||||||
|
`secretstore` (`KWalletAppID`, `KWalletFolder`, `BitwardenCommand`,
|
||||||
|
`BitwardenDebug`, `Shell`, `ExecutableResolver`). Si `ServiceName` est vide,
|
||||||
|
le nom du binaire déclaré dans le manifeste est utilisé.
|
||||||
|
|
||||||
## Flags
|
## Flags
|
||||||
|
|
||||||
- `--manifest` : chemin du `mcp.toml` à lire. Par défaut, `./mcp.toml`.
|
- `--manifest` : chemin du `mcp.toml` à lire. Par défaut, `./mcp.toml`.
|
||||||
|
|
@ -53,7 +97,12 @@ Pour remplacer un wrapper local du type `internal/manifest` :
|
||||||
`example.com/my-mcp/mcpgen`.
|
`example.com/my-mcp/mcpgen`.
|
||||||
4. Remplacer les appels `manifest.Load(...)` du wrapper par
|
4. Remplacer les appels `manifest.Load(...)` du wrapper par
|
||||||
`mcpgen.LoadManifest(...)`.
|
`mcpgen.LoadManifest(...)`.
|
||||||
5. Supprimer l'ancien wrapper manuel.
|
5. Remplacer les reconstructions locales d'options update par
|
||||||
|
`mcpgen.UpdateOptions(...)` ou `mcpgen.RunUpdate(...)`.
|
||||||
|
6. Remplacer les wrappers secret store qui ne font que brancher le loader par
|
||||||
|
`mcpgen.OpenSecretStore`, `mcpgen.DescribeSecretRuntime` et
|
||||||
|
`mcpgen.PreflightSecretStore`.
|
||||||
|
7. Supprimer l'ancien wrapper manuel.
|
||||||
|
|
||||||
Après génération, un simple `go build ./...` suffit. La compilation ne dépend
|
Après génération, un simple `go build ./...` suffit. La compilation ne dépend
|
||||||
pas de la commande `mcp-framework`.
|
pas de la commande `mcp-framework`.
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,8 @@ func Generate(options Options) (Result, error) {
|
||||||
return Result{}, err
|
return Result{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := manifest.Load(normalized.ManifestPath); err != nil {
|
manifestFile, err := manifest.Load(normalized.ManifestPath)
|
||||||
|
if err != nil {
|
||||||
return Result{}, err
|
return Result{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -45,7 +46,19 @@ func Generate(options Options) (Result, error) {
|
||||||
return Result{}, fmt.Errorf("read manifest %s: %w", normalized.ManifestPath, err)
|
return Result{}, fmt.Errorf("read manifest %s: %w", normalized.ManifestPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
content, err := renderManifestLoader(normalized.PackageName, string(manifestContent))
|
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 {
|
if err != nil {
|
||||||
return Result{}, err
|
return Result{}, err
|
||||||
}
|
}
|
||||||
|
|
@ -53,7 +66,22 @@ func Generate(options Options) (Result, error) {
|
||||||
files := []generatedFile{
|
files := []generatedFile{
|
||||||
{
|
{
|
||||||
Path: filepath.Join(normalized.PackageDir, "manifest.go"),
|
Path: filepath.Join(normalized.PackageDir, "manifest.go"),
|
||||||
Content: content,
|
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,
|
Mode: 0o644,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -189,6 +217,209 @@ func LoadManifest(startDir string) (fwmanifest.File, string, error) {
|
||||||
return string(formatted), nil
|
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 {
|
func writeGeneratedFile(path, content string, mode os.FileMode) error {
|
||||||
current, err := os.ReadFile(path)
|
current, err := os.ReadFile(path)
|
||||||
if err == nil && bytes.Equal(current, []byte(content)) {
|
if err == nil && bytes.Equal(current, []byte(content)) {
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ description = "Demo MCP"
|
||||||
t.Fatalf("Generate returned error: %v", err)
|
t.Fatalf("Generate returned error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !slices.Equal(result.Files, []string{filepath.Join("mcpgen", "manifest.go")}) {
|
if !slices.Equal(result.Files, defaultGeneratedFiles("mcpgen")) {
|
||||||
t.Fatalf("result files = %v", result.Files)
|
t.Fatalf("result files = %v", result.Files)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -49,6 +49,87 @@ description = "Demo MCP"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGenerateCreatesP1Helpers(t *testing.T) {
|
||||||
|
projectDir := newProject(t, `
|
||||||
|
binary_name = "demo-mcp"
|
||||||
|
docs_url = "https://docs.example.com/demo"
|
||||||
|
|
||||||
|
[update]
|
||||||
|
driver = "gitea"
|
||||||
|
repository = "org/demo-mcp"
|
||||||
|
base_url = "https://gitea.example.com"
|
||||||
|
asset_name_template = "{binary}-{os}-{arch}{ext}"
|
||||||
|
|
||||||
|
[secret_store]
|
||||||
|
backend_policy = "env-only"
|
||||||
|
|
||||||
|
[bootstrap]
|
||||||
|
description = "Demo MCP"
|
||||||
|
`)
|
||||||
|
|
||||||
|
result, err := Generate(Options{ProjectDir: projectDir})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Generate returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantFiles := []string{
|
||||||
|
filepath.Join("mcpgen", "manifest.go"),
|
||||||
|
filepath.Join("mcpgen", "metadata.go"),
|
||||||
|
filepath.Join("mcpgen", "secretstore.go"),
|
||||||
|
filepath.Join("mcpgen", "update.go"),
|
||||||
|
}
|
||||||
|
if !slices.Equal(result.Files, wantFiles) {
|
||||||
|
t.Fatalf("result files = %v, want %v", result.Files, wantFiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata, err := os.ReadFile(filepath.Join(projectDir, "mcpgen", "metadata.go"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReadFile metadata.go: %v", err)
|
||||||
|
}
|
||||||
|
for _, snippet := range []string{
|
||||||
|
`const BinaryName = "demo-mcp"`,
|
||||||
|
`const DefaultDescription = "Demo MCP"`,
|
||||||
|
`const DocsURL = "https://docs.example.com/demo"`,
|
||||||
|
"func BootstrapInfo(startDir string) (fwmanifest.BootstrapMetadata, string, error) {",
|
||||||
|
"func ScaffoldInfo(startDir string) (fwmanifest.ScaffoldMetadata, string, error) {",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(string(metadata), snippet) {
|
||||||
|
t.Fatalf("metadata.go missing snippet %q:\n%s", snippet, metadata)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update, err := os.ReadFile(filepath.Join(projectDir, "mcpgen", "update.go"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReadFile update.go: %v", err)
|
||||||
|
}
|
||||||
|
for _, snippet := range []string{
|
||||||
|
"func UpdateOptions(version string, stdout io.Writer) (fwupdate.Options, error) {",
|
||||||
|
"func UpdateOptionsFrom(startDir string, version string, stdout io.Writer) (fwupdate.Options, error) {",
|
||||||
|
"func RunUpdate(ctx context.Context, args []string, version string, stdout io.Writer) error {",
|
||||||
|
"ReleaseSource:",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(string(update), snippet) {
|
||||||
|
t.Fatalf("update.go missing snippet %q:\n%s", snippet, update)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
secretstore, err := os.ReadFile(filepath.Join(projectDir, "mcpgen", "secretstore.go"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReadFile secretstore.go: %v", err)
|
||||||
|
}
|
||||||
|
for _, snippet := range []string{
|
||||||
|
"type SecretStoreOptions struct {",
|
||||||
|
"func OpenSecretStore(options SecretStoreOptions) (fwsecretstore.Store, error) {",
|
||||||
|
"func DescribeSecretRuntime(options SecretStoreOptions) (fwsecretstore.RuntimeDescription, error) {",
|
||||||
|
"func PreflightSecretStore(options SecretStoreOptions) (fwsecretstore.PreflightReport, error) {",
|
||||||
|
"ManifestLoader:",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(string(secretstore), snippet) {
|
||||||
|
t.Fatalf("secretstore.go missing snippet %q:\n%s", snippet, secretstore)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGenerateIsIdempotentAndCheckDetectsDrift(t *testing.T) {
|
func TestGenerateIsIdempotentAndCheckDetectsDrift(t *testing.T) {
|
||||||
projectDir := newProject(t, `binary_name = "demo-mcp"`)
|
projectDir := newProject(t, `binary_name = "demo-mcp"`)
|
||||||
|
|
||||||
|
|
@ -106,7 +187,7 @@ func TestGenerateSupportsManifestAndPackageFlags(t *testing.T) {
|
||||||
t.Fatalf("Generate returned error: %v", err)
|
t.Fatalf("Generate returned error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !slices.Equal(result.Files, []string{filepath.Join("internal", "generated", "manifest.go")}) {
|
if !slices.Equal(result.Files, defaultGeneratedFiles(filepath.Join("internal", "generated"))) {
|
||||||
t.Fatalf("result files = %v", result.Files)
|
t.Fatalf("result files = %v", result.Files)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -135,6 +216,17 @@ func TestGeneratedLoaderFallsBackToEmbeddedManifest(t *testing.T) {
|
||||||
projectDir := newProject(t, `
|
projectDir := newProject(t, `
|
||||||
binary_name = "embedded-demo"
|
binary_name = "embedded-demo"
|
||||||
docs_url = "https://docs.example.com/embedded"
|
docs_url = "https://docs.example.com/embedded"
|
||||||
|
|
||||||
|
[update]
|
||||||
|
driver = "gitea"
|
||||||
|
repository = "org/embedded-demo"
|
||||||
|
base_url = "https://gitea.example.com"
|
||||||
|
|
||||||
|
[secret_store]
|
||||||
|
backend_policy = "env-only"
|
||||||
|
|
||||||
|
[bootstrap]
|
||||||
|
description = "Embedded Demo"
|
||||||
`)
|
`)
|
||||||
writeModule(t, projectDir)
|
writeModule(t, projectDir)
|
||||||
|
|
||||||
|
|
@ -146,7 +238,7 @@ docs_url = "https://docs.example.com/embedded"
|
||||||
t.Fatalf("Remove runtime manifest: %v", err)
|
t.Fatalf("Remove runtime manifest: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.Command("go", "test", "./...")
|
cmd := exec.Command("go", "test", "-mod=mod", "./...")
|
||||||
cmd.Dir = projectDir
|
cmd.Dir = projectDir
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -164,6 +256,15 @@ func newProject(t *testing.T, manifest string) string {
|
||||||
return projectDir
|
return projectDir
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func defaultGeneratedFiles(packageDir string) []string {
|
||||||
|
return []string{
|
||||||
|
filepath.Join(packageDir, "manifest.go"),
|
||||||
|
filepath.Join(packageDir, "metadata.go"),
|
||||||
|
filepath.Join(packageDir, "secretstore.go"),
|
||||||
|
filepath.Join(packageDir, "update.go"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func writeModule(t *testing.T, projectDir string) {
|
func writeModule(t *testing.T, projectDir string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
|
@ -172,7 +273,7 @@ func writeModule(t *testing.T, projectDir string) {
|
||||||
t.Fatalf("Abs repo root: %v", err)
|
t.Fatalf("Abs repo root: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
goMod := "module example.com/generated-demo\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/BurntSushi/toml v1.6.0\n\tgitea.lclr.dev/AI/mcp-framework v0.0.0\n)\n\nreplace gitea.lclr.dev/AI/mcp-framework => " + filepath.ToSlash(repoRoot) + "\n"
|
goMod := "module example.com/generated-demo\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/99designs/keyring v1.2.2\n\tgithub.com/BurntSushi/toml v1.6.0\n\tgitea.lclr.dev/AI/mcp-framework v0.0.0\n)\n\nreplace gitea.lclr.dev/AI/mcp-framework => " + filepath.ToSlash(repoRoot) + "\n"
|
||||||
if err := os.WriteFile(filepath.Join(projectDir, "go.mod"), []byte(goMod), 0o600); err != nil {
|
if err := os.WriteFile(filepath.Join(projectDir, "go.mod"), []byte(goMod), 0o600); err != nil {
|
||||||
t.Fatalf("WriteFile go.mod: %v", err)
|
t.Fatalf("WriteFile go.mod: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -188,13 +289,15 @@ func writeModule(t *testing.T, projectDir string) {
|
||||||
testFile := `package main
|
testFile := `package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
fwsecretstore "gitea.lclr.dev/AI/mcp-framework/secretstore"
|
||||||
"example.com/generated-demo/mcpgen"
|
"example.com/generated-demo/mcpgen"
|
||||||
fwmanifest "gitea.lclr.dev/AI/mcp-framework/manifest"
|
fwmanifest "gitea.lclr.dev/AI/mcp-framework/manifest"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGeneratedLoaderUsesEmbeddedManifest(t *testing.T) {
|
func TestGeneratedHelpersUseEmbeddedManifest(t *testing.T) {
|
||||||
file, source, err := mcpgen.LoadManifest(".")
|
file, source, err := mcpgen.LoadManifest(".")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("LoadManifest returned error: %v", err)
|
t.Fatalf("LoadManifest returned error: %v", err)
|
||||||
|
|
@ -205,6 +308,50 @@ func TestGeneratedLoaderUsesEmbeddedManifest(t *testing.T) {
|
||||||
if file.BinaryName != "embedded-demo" {
|
if file.BinaryName != "embedded-demo" {
|
||||||
t.Fatalf("binary name = %q", file.BinaryName)
|
t.Fatalf("binary name = %q", file.BinaryName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
info, source, err := mcpgen.BootstrapInfo(".")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("BootstrapInfo returned error: %v", err)
|
||||||
|
}
|
||||||
|
if source != fwmanifest.EmbeddedSource {
|
||||||
|
t.Fatalf("bootstrap source = %q, want %q", source, fwmanifest.EmbeddedSource)
|
||||||
|
}
|
||||||
|
if info.Description != "Embedded Demo" {
|
||||||
|
t.Fatalf("description = %q", info.Description)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateOptions, err := mcpgen.UpdateOptions("1.2.3", io.Discard)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("UpdateOptions returned error: %v", err)
|
||||||
|
}
|
||||||
|
if updateOptions.CurrentVersion != "1.2.3" {
|
||||||
|
t.Fatalf("current version = %q", updateOptions.CurrentVersion)
|
||||||
|
}
|
||||||
|
if updateOptions.BinaryName != "embedded-demo" {
|
||||||
|
t.Fatalf("update binary name = %q", updateOptions.BinaryName)
|
||||||
|
}
|
||||||
|
if updateOptions.ReleaseSource.Repository != "org/embedded-demo" {
|
||||||
|
t.Fatalf("release repository = %q", updateOptions.ReleaseSource.Repository)
|
||||||
|
}
|
||||||
|
|
||||||
|
store, err := mcpgen.OpenSecretStore(mcpgen.SecretStoreOptions{
|
||||||
|
LookupEnv: func(name string) (string, bool) {
|
||||||
|
return "secret-from-env", true
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("OpenSecretStore returned error: %v", err)
|
||||||
|
}
|
||||||
|
value, err := store.GetSecret("profile/default/api-token")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetSecret returned error: %v", err)
|
||||||
|
}
|
||||||
|
if value != "secret-from-env" {
|
||||||
|
t.Fatalf("secret value = %q", value)
|
||||||
|
}
|
||||||
|
if fwsecretstore.EffectiveBackendPolicy(store) != fwsecretstore.BackendEnvOnly {
|
||||||
|
t.Fatalf("effective backend = %q", fwsecretstore.EffectiveBackendPolicy(store))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(filepath.Join(projectDir, "main_test.go"), []byte(testFile), 0o600); err != nil {
|
if err := os.WriteFile(filepath.Join(projectDir, "main_test.go"), []byte(testFile), 0o600); err != nil {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue