Token files with trailing newlines caused gRPC "non-printable ASCII characters" errors in the authorization header. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
74 lines
2.3 KiB
Go
74 lines
2.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
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"
|
|
)
|
|
|
|
// 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 strings.TrimSpace(string(token)), nil
|
|
}
|