P2.1 + P3.1: Agent skeleton and CLI skeleton
Agent (P2.1): Agent struct with registry DB, runtime, and logger. gRPC server with TLS 1.3 and MCIAS auth interceptor. Graceful shutdown on SIGINT/SIGTERM. All RPCs return Unimplemented until handlers are built in P2.2-P2.9. CLI (P3.1): Full command tree with all 15 subcommands as stubs (login, deploy, stop, start, restart, list, ps, status, sync, adopt, service show/edit/export, push, pull, node list/add/remove). gRPC dial helper with TLS, CA cert, and bearer token attachment. Both gates for parallel Phase 2+3 work are now open. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,8 +2,11 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"git.wntrmute.dev/kyle/mcp/internal/agent"
|
||||
"git.wntrmute.dev/kyle/mcp/internal/config"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -27,7 +30,20 @@ func main() {
|
||||
},
|
||||
})
|
||||
|
||||
root.AddCommand(&cobra.Command{
|
||||
Use: "server",
|
||||
Short: "Start the agent server",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := config.LoadAgentConfig(cfgPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load config: %w", err)
|
||||
}
|
||||
return agent.Run(cfg)
|
||||
},
|
||||
})
|
||||
|
||||
if err := root.Execute(); err != nil {
|
||||
log.Fatal(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
79
cmd/mcp/dial.go
Normal file
79
cmd/mcp/dial.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
mcpv1 "git.wntrmute.dev/kyle/mcp/gen/mcp/v1"
|
||||
"git.wntrmute.dev/kyle/mcp/internal/config"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
// Ensure dial helpers are referenced to satisfy linters until CLI commands
|
||||
// are implemented. This will be removed when the first command uses dialAgent.
|
||||
var (
|
||||
_ = dialAgent
|
||||
_ = loadBearerToken
|
||||
)
|
||||
|
||||
// dialAgent connects to an agent at the given address and returns a gRPC
|
||||
// client. The connection uses TLS and attaches the bearer token to every RPC.
|
||||
func dialAgent(address string, cfg *config.CLIConfig) (mcpv1.McpAgentServiceClient, *grpc.ClientConn, error) {
|
||||
tlsConfig := &tls.Config{
|
||||
MinVersion: tls.VersionTLS13,
|
||||
}
|
||||
|
||||
if cfg.MCIAS.CACert != "" {
|
||||
caCert, err := os.ReadFile(cfg.MCIAS.CACert) //nolint:gosec // trusted config path
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("read CA cert %q: %w", cfg.MCIAS.CACert, err)
|
||||
}
|
||||
pool := x509.NewCertPool()
|
||||
if !pool.AppendCertsFromPEM(caCert) {
|
||||
return nil, nil, fmt.Errorf("invalid CA cert %q", cfg.MCIAS.CACert)
|
||||
}
|
||||
tlsConfig.RootCAs = pool
|
||||
}
|
||||
|
||||
token, err := loadBearerToken(cfg)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("load token: %w", err)
|
||||
}
|
||||
|
||||
conn, err := grpc.NewClient(
|
||||
address,
|
||||
grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)),
|
||||
grpc.WithUnaryInterceptor(tokenInterceptor(token)),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("dial %q: %w", address, err)
|
||||
}
|
||||
|
||||
return mcpv1.NewMcpAgentServiceClient(conn), conn, nil
|
||||
}
|
||||
|
||||
// tokenInterceptor returns a gRPC client interceptor that attaches the
|
||||
// bearer token to outgoing RPC metadata.
|
||||
func tokenInterceptor(token string) grpc.UnaryClientInterceptor {
|
||||
return func(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
|
||||
ctx = metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer "+token)
|
||||
return invoker(ctx, method, req, reply, cc, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
// loadBearerToken reads the token from file or env var.
|
||||
func loadBearerToken(cfg *config.CLIConfig) (string, error) {
|
||||
if token := os.Getenv("MCP_TOKEN"); token != "" {
|
||||
return token, nil
|
||||
}
|
||||
token, err := os.ReadFile(cfg.Auth.TokenPath) //nolint:gosec // trusted config path
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read token from %q: %w (run 'mcp login' first)", cfg.Auth.TokenPath, err)
|
||||
}
|
||||
return string(token), nil
|
||||
}
|
||||
234
cmd/mcp/main.go
234
cmd/mcp/main.go
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
@@ -19,15 +20,238 @@ func main() {
|
||||
}
|
||||
root.PersistentFlags().StringVarP(&cfgPath, "config", "c", "", "config file path")
|
||||
|
||||
root.AddCommand(&cobra.Command{
|
||||
root.AddCommand(versionCmd())
|
||||
root.AddCommand(loginCmd())
|
||||
root.AddCommand(deployCmd())
|
||||
root.AddCommand(stopCmd())
|
||||
root.AddCommand(startCmd())
|
||||
root.AddCommand(restartCmd())
|
||||
root.AddCommand(listCmd())
|
||||
root.AddCommand(psCmd())
|
||||
root.AddCommand(statusCmd())
|
||||
root.AddCommand(syncCmd())
|
||||
root.AddCommand(adoptCmd())
|
||||
root.AddCommand(serviceCmd())
|
||||
root.AddCommand(pushCmd())
|
||||
root.AddCommand(pullCmd())
|
||||
root.AddCommand(nodeCmd())
|
||||
|
||||
if err := root.Execute(); err != nil {
|
||||
log.Fatal(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func versionCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print version",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Println(version)
|
||||
},
|
||||
})
|
||||
|
||||
if err := root.Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func loginCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "login",
|
||||
Short: "Authenticate to MCIAS, store token",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func deployCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "deploy <service>[/<component>]",
|
||||
Short: "Deploy service from service definition",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringP("file", "f", "", "service definition file")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func stopCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "stop <service>",
|
||||
Short: "Stop all components, set active=false",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func startCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "start <service>",
|
||||
Short: "Start all components, set active=true",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func restartCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "restart <service>",
|
||||
Short: "Restart all components",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func listCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List services from all agents (registry, no runtime query)",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func psCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "ps",
|
||||
Short: "Live check: query runtime on all agents",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func statusCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "status [service]",
|
||||
Short: "Full picture: live query + drift + recent events",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func syncCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "sync",
|
||||
Short: "Push service definitions to agents (update desired state)",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func adoptCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "adopt <service>",
|
||||
Short: "Adopt all <service>-* containers into a service",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func serviceCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "service",
|
||||
Short: "Service definition management",
|
||||
}
|
||||
|
||||
show := &cobra.Command{
|
||||
Use: "show <service>",
|
||||
Short: "Print current spec from agent registry",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
},
|
||||
}
|
||||
|
||||
edit := &cobra.Command{
|
||||
Use: "edit <service>",
|
||||
Short: "Open service definition in $EDITOR",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
},
|
||||
}
|
||||
|
||||
export := &cobra.Command{
|
||||
Use: "export <service>",
|
||||
Short: "Write agent registry spec to local service file",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
},
|
||||
}
|
||||
export.Flags().StringP("file", "f", "", "output file path")
|
||||
|
||||
cmd.AddCommand(show, edit, export)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func pushCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "push <local-file> <service> [path]",
|
||||
Short: "Copy a local file into /srv/<service>/[path]",
|
||||
Args: cobra.RangeArgs(2, 3),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func pullCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "pull <service> <path> [local-file]",
|
||||
Short: "Copy a file from /srv/<service>/<path> to local",
|
||||
Args: cobra.RangeArgs(2, 3),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func nodeCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "node",
|
||||
Short: "Node management",
|
||||
}
|
||||
|
||||
list := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List registered nodes",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
},
|
||||
}
|
||||
|
||||
add := &cobra.Command{
|
||||
Use: "add <name> <address>",
|
||||
Short: "Register a node",
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
},
|
||||
}
|
||||
|
||||
remove := &cobra.Command{
|
||||
Use: "remove <name>",
|
||||
Short: "Deregister a node",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
},
|
||||
}
|
||||
|
||||
cmd.AddCommand(list, add, remove)
|
||||
return cmd
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user