New server-streaming Logs RPC streams container output to the CLI. Supports --tail/-n, --follow/-f, --timestamps/-t, --since. Detects journald log driver and falls back to journalctl (podman logs can't read journald outside the originating user session). New containers default to k8s-file via mcp user's containers.conf. Also adds stream auth interceptor for the agent gRPC server (required for streaming RPCs). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
84 lines
2.8 KiB
Go
84 lines
2.8 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
mcpv1 "git.wntrmute.dev/mc/mcp/gen/mcp/v1"
|
|
"git.wntrmute.dev/mc/mcp/internal/config"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials"
|
|
"google.golang.org/grpc/metadata"
|
|
)
|
|
|
|
// 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)),
|
|
grpc.WithStreamInterceptor(streamTokenInterceptor(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...)
|
|
}
|
|
}
|
|
|
|
// streamTokenInterceptor returns a gRPC client stream interceptor that
|
|
// attaches the bearer token to outgoing stream metadata.
|
|
func streamTokenInterceptor(token string) grpc.StreamClientInterceptor {
|
|
return func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
|
|
ctx = metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer "+token)
|
|
return streamer(ctx, desc, cc, method, 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 strings.TrimSpace(string(token)), nil
|
|
}
|