feat: adopt mcp-framework rc2 bootstrap commands
This commit is contained in:
parent
2aa5e92b50
commit
d1fd485fb2
5 changed files with 150 additions and 23 deletions
|
|
@ -17,6 +17,9 @@ Le binaire s’appuie maintenant sur [`mcp-framework`](../mcp-framework) pour :
|
|||
- `email-mcp mcp` : lance le serveur MCP sur `stdin/stdout`
|
||||
- `email-mcp doctor` : diagnostique la configuration locale, le wallet, le manifeste et l’accès IMAP
|
||||
- `email-mcp update` : met à jour le binaire courant depuis la dernière release
|
||||
- `email-mcp version` : affiche la version du binaire
|
||||
|
||||
La commande `email-mcp help` (ou `-h` / `--help`) affiche l’aide globale.
|
||||
|
||||
## Outils MCP
|
||||
|
||||
|
|
|
|||
2
go.mod
2
go.mod
|
|
@ -3,7 +3,7 @@ module email-mcp
|
|||
go 1.25.0
|
||||
|
||||
require (
|
||||
gitea.lclr.dev/AI/mcp-framework v1.2.0-rc1
|
||||
gitea.lclr.dev/AI/mcp-framework v1.2.0-rc2
|
||||
github.com/emersion/go-imap/v2 v2.0.0-beta.8
|
||||
github.com/emersion/go-message v0.18.2
|
||||
github.com/godbus/dbus/v5 v5.2.2
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -1,5 +1,5 @@
|
|||
gitea.lclr.dev/AI/mcp-framework v1.2.0-rc1 h1:S4YZ7G9b5RI7DmgTTEzWJKsWFf9Z1guPjxyajkcNLI0=
|
||||
gitea.lclr.dev/AI/mcp-framework v1.2.0-rc1/go.mod h1:kUVMrL3/UBYgjOsW7sJCs3V0pO0qoJJMpIpueoTsoA4=
|
||||
gitea.lclr.dev/AI/mcp-framework v1.2.0-rc2 h1:nzeW1JkGPV/+Hhhtdy7EWWeDQNjt36qMeVQjJYmGCQE=
|
||||
gitea.lclr.dev/AI/mcp-framework v1.2.0-rc2/go.mod h1:kUVMrL3/UBYgjOsW7sJCs3V0pO0qoJJMpIpueoTsoA4=
|
||||
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs=
|
||||
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4=
|
||||
github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0=
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
frameworkbootstrap "gitea.lclr.dev/AI/mcp-framework/bootstrap"
|
||||
frameworkcli "gitea.lclr.dev/AI/mcp-framework/cli"
|
||||
frameworkconfig "gitea.lclr.dev/AI/mcp-framework/config"
|
||||
frameworkmanifest "gitea.lclr.dev/AI/mcp-framework/manifest"
|
||||
|
|
@ -23,6 +24,7 @@ import (
|
|||
const (
|
||||
binaryName = "email-mcp"
|
||||
defaultProfileEnv = "EMAIL_MCP_PROFILE"
|
||||
binaryDescription = "Local MCP server to read an IMAP mailbox."
|
||||
)
|
||||
|
||||
type MCPRunner interface {
|
||||
|
|
@ -112,22 +114,119 @@ func NewAppWithDependencies(
|
|||
}
|
||||
|
||||
func (a *App) Run(args []string) error {
|
||||
if len(args) == 0 {
|
||||
return fmt.Errorf("usage: email-mcp <config|setup|mcp|doctor|update>")
|
||||
if isDoctorHelpCommand(args) {
|
||||
return a.printDoctorHelp()
|
||||
}
|
||||
|
||||
switch args[0] {
|
||||
case "config", "setup":
|
||||
return a.runConfig(context.Background(), args[0], args[1:])
|
||||
case "mcp":
|
||||
return a.runMCP(context.Background(), args[1:])
|
||||
case "doctor":
|
||||
if len(args) > 0 && strings.TrimSpace(args[0]) == "doctor" {
|
||||
return a.runDoctor(context.Background(), args[1:])
|
||||
case "update":
|
||||
return a.runUpdate(context.Background(), args[1:])
|
||||
default:
|
||||
return fmt.Errorf("unknown command: %s", args[0])
|
||||
}
|
||||
|
||||
if isGlobalHelpCommand(args) {
|
||||
return a.printGlobalHelp()
|
||||
}
|
||||
|
||||
return a.runBootstrap(context.Background(), args)
|
||||
}
|
||||
|
||||
func (a *App) runBootstrap(ctx context.Context, args []string) error {
|
||||
return frameworkbootstrap.Run(ctx, frameworkbootstrap.Options{
|
||||
BinaryName: binaryName,
|
||||
Description: binaryDescription,
|
||||
Version: a.version,
|
||||
Args: args,
|
||||
Stdin: a.stdin,
|
||||
Stdout: a.stdout,
|
||||
Stderr: a.stderr,
|
||||
Hooks: frameworkbootstrap.Hooks{
|
||||
Setup: func(ctx context.Context, inv frameworkbootstrap.Invocation) error {
|
||||
return a.runConfig(ctx, frameworkbootstrap.CommandSetup, inv.Args)
|
||||
},
|
||||
MCP: func(ctx context.Context, inv frameworkbootstrap.Invocation) error {
|
||||
return a.runMCP(ctx, inv.Args)
|
||||
},
|
||||
Config: func(ctx context.Context, inv frameworkbootstrap.Invocation) error {
|
||||
return a.runConfig(ctx, frameworkbootstrap.CommandConfig, inv.Args)
|
||||
},
|
||||
Update: func(ctx context.Context, inv frameworkbootstrap.Invocation) error {
|
||||
return a.runUpdate(ctx, inv.Args)
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func isGlobalHelpCommand(args []string) bool {
|
||||
if len(args) == 0 {
|
||||
return true
|
||||
}
|
||||
if len(args) != 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
switch strings.TrimSpace(args[0]) {
|
||||
case "help", "-h", "--help":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isDoctorHelpCommand(args []string) bool {
|
||||
if len(args) != 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
first := strings.TrimSpace(args[0])
|
||||
second := strings.TrimSpace(args[1])
|
||||
|
||||
if first == "help" && second == "doctor" {
|
||||
return true
|
||||
}
|
||||
if first == "doctor" && (second == "-h" || second == "--help") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *App) printGlobalHelp() error {
|
||||
if _, err := fmt.Fprintf(a.stdout, "%s\n\n", binaryDescription); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := fmt.Fprintf(a.stdout, "Usage:\n %s <command> [args]\n\n", binaryName); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := fmt.Fprintln(a.stdout, "Common commands:"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
commands := []struct {
|
||||
name string
|
||||
description string
|
||||
}{
|
||||
{name: "setup", description: "Initialize or update local configuration."},
|
||||
{name: "mcp", description: "Run the MCP server over stdio."},
|
||||
{name: "config", description: "Inspect or update configuration."},
|
||||
{name: "doctor", description: "Run local diagnostics."},
|
||||
{name: "update", description: "Run the self-update flow."},
|
||||
{name: "version", description: "Print the binary version."},
|
||||
}
|
||||
for _, command := range commands {
|
||||
if _, err := fmt.Fprintf(a.stdout, " %-7s %s\n", command.name, command.description); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_, err := fmt.Fprintf(a.stdout, "\nDetailed help: %s help <command>\n", binaryName)
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *App) printDoctorHelp() error {
|
||||
_, err := fmt.Fprintf(
|
||||
a.stdout,
|
||||
"Usage:\n %s doctor [--profile NAME]\n\nRun local diagnostics for config, wallet, manifest, and IMAP connectivity.\n",
|
||||
binaryName,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *App) runConfig(ctx context.Context, command string, args []string) error {
|
||||
|
|
|
|||
|
|
@ -142,17 +142,42 @@ func TestAppRunRejectsUnknownCommand(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAppRunShowsUsageWhenNoArgsProvided(t *testing.T) {
|
||||
app := NewAppWithDependencies(nil, nil, nil, nil, nil, nil, nil, nil, nil, &bytes.Buffer{}, "dev")
|
||||
output := &bytes.Buffer{}
|
||||
app := NewAppWithDependencies(nil, nil, nil, nil, nil, nil, nil, nil, output, &bytes.Buffer{}, "dev")
|
||||
|
||||
err := app.Run(nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing command")
|
||||
if err := app.Run(nil); err != nil {
|
||||
t.Fatalf("expected help to be rendered, got error %v", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "usage:") {
|
||||
t.Fatalf("expected usage text in error, got %q", err.Error())
|
||||
|
||||
text := output.String()
|
||||
for _, snippet := range []string{"Usage:", "doctor", "version"} {
|
||||
if !strings.Contains(text, snippet) {
|
||||
t.Fatalf("help output missing %q: %q", snippet, text)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(err.Error(), "doctor") {
|
||||
t.Fatalf("expected usage to mention doctor, got %q", err.Error())
|
||||
}
|
||||
|
||||
func TestAppRunVersionPrintsBuildVersion(t *testing.T) {
|
||||
output := &bytes.Buffer{}
|
||||
app := NewAppWithDependencies(nil, nil, nil, nil, nil, nil, nil, nil, output, &bytes.Buffer{}, "v1.2.3")
|
||||
|
||||
if err := app.Run([]string{"version"}); err != nil {
|
||||
t.Fatalf("version returned error: %v", err)
|
||||
}
|
||||
if got := output.String(); got != "v1.2.3\n" {
|
||||
t.Fatalf("version output = %q, want %q", got, "v1.2.3\n")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppRunDoctorHelp(t *testing.T) {
|
||||
output := &bytes.Buffer{}
|
||||
app := NewAppWithDependencies(nil, nil, nil, nil, nil, nil, nil, nil, output, &bytes.Buffer{}, "dev")
|
||||
|
||||
if err := app.Run([]string{"doctor", "--help"}); err != nil {
|
||||
t.Fatalf("doctor help returned error: %v", err)
|
||||
}
|
||||
if got := output.String(); !strings.Contains(got, "email-mcp doctor [--profile NAME]") {
|
||||
t.Fatalf("unexpected doctor help output: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue