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:
2026-03-26 11:51:03 -07:00
parent 15b8823810
commit 53535f1e96
5 changed files with 445 additions and 7 deletions

View File

@@ -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
View 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
}

View File

@@ -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
}