Some checks failed
CI / build-and-test (push) Failing after -32m50s
* Add function to extract MCP session ID from request headers and query parameters * Update access log to include MCP session ID fix(cli): simplify project lookup logic * Refactor project retrieval to prefer GUID lookup when input is a valid UUID * Introduce separate functions for fetching projects by GUID and name
152 lines
3.6 KiB
Go
152 lines
3.6 KiB
Go
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var (
|
|
cfgFile string
|
|
serverFlag string
|
|
tokenFlag string
|
|
outputFlag string
|
|
verbose bool
|
|
cfg Config
|
|
)
|
|
|
|
const cliUserAgent = "amcs-cli/0.0.1"
|
|
|
|
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")
|
|
rootCmd.PersistentFlags().BoolVar(&verbose, "verbose", false, "Enable verbose logging to stderr")
|
|
}
|
|
|
|
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_SERVER")); v != "" {
|
|
cfg.Server = v
|
|
}
|
|
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_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(clone.Header.Get("User-Agent")) == "" {
|
|
clone.Header.Set("User-Agent", cliUserAgent)
|
|
}
|
|
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
|
|
}
|
|
verboseLogf("connecting to %s", endpointURL())
|
|
client := mcp.NewClient(&mcp.Implementation{Name: "amcs-cli", Version: "0.0.1"}, nil)
|
|
transport := &mcp.StreamableClientTransport{
|
|
Endpoint: endpointURL(),
|
|
HTTPClient: newHTTPClient(),
|
|
DisableStandaloneSSE: true,
|
|
}
|
|
session, err := client.Connect(ctx, transport, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("connect to AMCS server: %w", err)
|
|
}
|
|
verboseLogf("connected to %s", endpointURL())
|
|
return session, nil
|
|
}
|
|
|
|
func verboseLogf(format string, args ...any) {
|
|
if !verbose {
|
|
return
|
|
}
|
|
_, _ = fmt.Fprintf(os.Stderr, "[amcs-cli] "+format+"\n", args...)
|
|
}
|