- cmd/amcs-cli: new CLI tool for human/AI MCP tool access - amcs-cli tools: list all tools from remote MCP server - amcs-cli call <tool> --arg k=v: invoke a tool, print JSON/YAML result - amcs-cli stdio: stdio→HTTP MCP bridge for AI clients - Config: ~/.config/amcs/config.yaml, AMCS_URL/AMCS_TOKEN env vars, --server/--token flags - Token never logged in errors - Makefile: add build-cli target Closes #18
135 lines
3.1 KiB
Go
135 lines
3.1 KiB
Go
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var (
|
|
cfgFile string
|
|
serverFlag string
|
|
tokenFlag string
|
|
outputFlag string
|
|
cfg Config
|
|
)
|
|
|
|
var rootCmd = &cobra.Command{
|
|
Use: "amcs-cli",
|
|
Short: "CLI for connecting to a remote AMCS MCP server",
|
|
SilenceUsage: true,
|
|
SilenceErrors: true,
|
|
PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
|
|
return loadConfig()
|
|
},
|
|
}
|
|
|
|
func Execute() {
|
|
if err := rootCmd.Execute(); err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func init() {
|
|
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "Path to config file")
|
|
rootCmd.PersistentFlags().StringVar(&serverFlag, "server", "", "AMCS server URL")
|
|
rootCmd.PersistentFlags().StringVar(&tokenFlag, "token", "", "AMCS bearer token")
|
|
rootCmd.PersistentFlags().StringVar(&outputFlag, "output", "json", "Output format: json or yaml")
|
|
}
|
|
|
|
func loadConfig() error {
|
|
path, err := resolveConfigPath(cfgFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
loaded, err := loadConfigFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cfg = loaded
|
|
if v := strings.TrimSpace(os.Getenv("AMCS_URL")); v != "" {
|
|
cfg.Server = v
|
|
}
|
|
if v := strings.TrimSpace(os.Getenv("AMCS_TOKEN")); v != "" {
|
|
cfg.Token = v
|
|
}
|
|
if v := strings.TrimSpace(serverFlag); v != "" {
|
|
cfg.Server = v
|
|
}
|
|
if v := strings.TrimSpace(tokenFlag); v != "" {
|
|
cfg.Token = v
|
|
}
|
|
outputFlag = strings.ToLower(strings.TrimSpace(outputFlag))
|
|
if outputFlag != "json" && outputFlag != "yaml" {
|
|
return fmt.Errorf("invalid --output %q: must be json or yaml", outputFlag)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func requireServer() error {
|
|
if strings.TrimSpace(cfg.Server) == "" {
|
|
return fmt.Errorf("server URL is required; set --server, AMCS_URL, or config server")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func endpointURL() string {
|
|
base := strings.TrimRight(strings.TrimSpace(cfg.Server), "/")
|
|
if strings.HasSuffix(base, "/mcp") {
|
|
return base
|
|
}
|
|
return base + "/mcp"
|
|
}
|
|
|
|
func newHTTPClient() *http.Client {
|
|
return &http.Client{
|
|
Timeout: 0,
|
|
Transport: &bearerTransport{
|
|
base: http.DefaultTransport,
|
|
token: cfg.Token,
|
|
},
|
|
}
|
|
}
|
|
|
|
type bearerTransport struct {
|
|
base http.RoundTripper
|
|
token string
|
|
}
|
|
|
|
func (t *bearerTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
base := t.base
|
|
if base == nil {
|
|
base = http.DefaultTransport
|
|
}
|
|
clone := req.Clone(req.Context())
|
|
if strings.TrimSpace(t.token) != "" {
|
|
clone.Header.Set("Authorization", "Bearer "+t.token)
|
|
}
|
|
return base.RoundTrip(clone)
|
|
}
|
|
|
|
func connectRemote(ctx context.Context) (*mcp.ClientSession, error) {
|
|
if err := requireServer(); err != nil {
|
|
return nil, err
|
|
}
|
|
client := mcp.NewClient(&mcp.Implementation{Name: "amcs-cli", Version: "0.0.1"}, nil)
|
|
transport := &mcp.StreamableClientTransport{
|
|
Endpoint: endpointURL(),
|
|
HTTPClient: newHTTPClient(),
|
|
}
|
|
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
|
defer cancel()
|
|
session, err := client.Connect(ctx, transport, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("connect to AMCS server: %w", err)
|
|
}
|
|
return session, nil
|
|
}
|