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 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 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 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
|
## Outils MCP
|
||||||
|
|
||||||
|
|
|
||||||
2
go.mod
2
go.mod
|
|
@ -3,7 +3,7 @@ module email-mcp
|
||||||
go 1.25.0
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
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-imap/v2 v2.0.0-beta.8
|
||||||
github.com/emersion/go-message v0.18.2
|
github.com/emersion/go-message v0.18.2
|
||||||
github.com/godbus/dbus/v5 v5.2.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-rc2 h1:nzeW1JkGPV/+Hhhtdy7EWWeDQNjt36qMeVQjJYmGCQE=
|
||||||
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/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 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs=
|
||||||
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4=
|
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=
|
github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0=
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
frameworkbootstrap "gitea.lclr.dev/AI/mcp-framework/bootstrap"
|
||||||
frameworkcli "gitea.lclr.dev/AI/mcp-framework/cli"
|
frameworkcli "gitea.lclr.dev/AI/mcp-framework/cli"
|
||||||
frameworkconfig "gitea.lclr.dev/AI/mcp-framework/config"
|
frameworkconfig "gitea.lclr.dev/AI/mcp-framework/config"
|
||||||
frameworkmanifest "gitea.lclr.dev/AI/mcp-framework/manifest"
|
frameworkmanifest "gitea.lclr.dev/AI/mcp-framework/manifest"
|
||||||
|
|
@ -23,6 +24,7 @@ import (
|
||||||
const (
|
const (
|
||||||
binaryName = "email-mcp"
|
binaryName = "email-mcp"
|
||||||
defaultProfileEnv = "EMAIL_MCP_PROFILE"
|
defaultProfileEnv = "EMAIL_MCP_PROFILE"
|
||||||
|
binaryDescription = "Local MCP server to read an IMAP mailbox."
|
||||||
)
|
)
|
||||||
|
|
||||||
type MCPRunner interface {
|
type MCPRunner interface {
|
||||||
|
|
@ -112,22 +114,119 @@ func NewAppWithDependencies(
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) Run(args []string) error {
|
func (a *App) Run(args []string) error {
|
||||||
if len(args) == 0 {
|
if isDoctorHelpCommand(args) {
|
||||||
return fmt.Errorf("usage: email-mcp <config|setup|mcp|doctor|update>")
|
return a.printDoctorHelp()
|
||||||
}
|
}
|
||||||
|
|
||||||
switch args[0] {
|
if len(args) > 0 && strings.TrimSpace(args[0]) == "doctor" {
|
||||||
case "config", "setup":
|
|
||||||
return a.runConfig(context.Background(), args[0], args[1:])
|
|
||||||
case "mcp":
|
|
||||||
return a.runMCP(context.Background(), args[1:])
|
|
||||||
case "doctor":
|
|
||||||
return a.runDoctor(context.Background(), args[1:])
|
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 {
|
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) {
|
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 := app.Run(nil); err != nil {
|
||||||
if err == nil {
|
t.Fatalf("expected help to be rendered, got error %v", err)
|
||||||
t.Fatal("expected error for missing command")
|
|
||||||
}
|
}
|
||||||
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