Add mcp logs command for streaming container logs

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>
This commit is contained in:
2026-03-28 17:54:48 -07:00
parent 18365cc0a8
commit 14b978861f
10 changed files with 490 additions and 25 deletions

View File

@@ -2224,6 +2224,134 @@ func (x *PurgeResult) GetReason() string {
return ""
}
type LogsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Service string `protobuf:"bytes,1,opt,name=service,proto3" json:"service,omitempty"`
Component string `protobuf:"bytes,2,opt,name=component,proto3" json:"component,omitempty"` // optional; defaults to first/only component
Tail int32 `protobuf:"varint,3,opt,name=tail,proto3" json:"tail,omitempty"` // number of lines from the end (0 = all)
Follow bool `protobuf:"varint,4,opt,name=follow,proto3" json:"follow,omitempty"` // stream new output
Timestamps bool `protobuf:"varint,5,opt,name=timestamps,proto3" json:"timestamps,omitempty"` // prepend timestamps
Since string `protobuf:"bytes,6,opt,name=since,proto3" json:"since,omitempty"` // show logs since (e.g., "2h", "2026-03-28T00:00:00Z")
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *LogsRequest) Reset() {
*x = LogsRequest{}
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[39]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *LogsRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*LogsRequest) ProtoMessage() {}
func (x *LogsRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[39]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use LogsRequest.ProtoReflect.Descriptor instead.
func (*LogsRequest) Descriptor() ([]byte, []int) {
return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{39}
}
func (x *LogsRequest) GetService() string {
if x != nil {
return x.Service
}
return ""
}
func (x *LogsRequest) GetComponent() string {
if x != nil {
return x.Component
}
return ""
}
func (x *LogsRequest) GetTail() int32 {
if x != nil {
return x.Tail
}
return 0
}
func (x *LogsRequest) GetFollow() bool {
if x != nil {
return x.Follow
}
return false
}
func (x *LogsRequest) GetTimestamps() bool {
if x != nil {
return x.Timestamps
}
return false
}
func (x *LogsRequest) GetSince() string {
if x != nil {
return x.Since
}
return ""
}
type LogsResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *LogsResponse) Reset() {
*x = LogsResponse{}
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[40]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *LogsResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*LogsResponse) ProtoMessage() {}
func (x *LogsResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcp_v1_mcp_proto_msgTypes[40]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use LogsResponse.ProtoReflect.Descriptor instead.
func (*LogsResponse) Descriptor() ([]byte, []int) {
return file_proto_mcp_v1_mcp_proto_rawDescGZIP(), []int{40}
}
func (x *LogsResponse) GetData() []byte {
if x != nil {
return x.Data
}
return nil
}
var File_proto_mcp_v1_mcp_proto protoreflect.FileDescriptor
const file_proto_mcp_v1_mcp_proto_rawDesc = "" +
@@ -2371,7 +2499,18 @@ const file_proto_mcp_v1_mcp_proto_rawDesc = "" +
"\aservice\x18\x01 \x01(\tR\aservice\x12\x1c\n" +
"\tcomponent\x18\x02 \x01(\tR\tcomponent\x12\x16\n" +
"\x06purged\x18\x03 \x01(\bR\x06purged\x12\x16\n" +
"\x06reason\x18\x04 \x01(\tR\x06reason2\x93\b\n" +
"\x06reason\x18\x04 \x01(\tR\x06reason\"\xa7\x01\n" +
"\vLogsRequest\x12\x18\n" +
"\aservice\x18\x01 \x01(\tR\aservice\x12\x1c\n" +
"\tcomponent\x18\x02 \x01(\tR\tcomponent\x12\x12\n" +
"\x04tail\x18\x03 \x01(\x05R\x04tail\x12\x16\n" +
"\x06follow\x18\x04 \x01(\bR\x06follow\x12\x1e\n" +
"\n" +
"timestamps\x18\x05 \x01(\bR\n" +
"timestamps\x12\x14\n" +
"\x05since\x18\x06 \x01(\tR\x05since\"\"\n" +
"\fLogsResponse\x12\x12\n" +
"\x04data\x18\x01 \x01(\fR\x04data2\xc8\b\n" +
"\x0fMcpAgentService\x127\n" +
"\x06Deploy\x12\x15.mcp.v1.DeployRequest\x1a\x16.mcp.v1.DeployResponse\x12R\n" +
"\x0fUndeployService\x12\x1e.mcp.v1.UndeployServiceRequest\x1a\x1f.mcp.v1.UndeployServiceResponse\x12F\n" +
@@ -2387,7 +2526,8 @@ const file_proto_mcp_v1_mcp_proto_rawDesc = "" +
"\bPushFile\x12\x17.mcp.v1.PushFileRequest\x1a\x18.mcp.v1.PushFileResponse\x12=\n" +
"\bPullFile\x12\x17.mcp.v1.PullFileRequest\x1a\x18.mcp.v1.PullFileResponse\x12C\n" +
"\n" +
"NodeStatus\x12\x19.mcp.v1.NodeStatusRequest\x1a\x1a.mcp.v1.NodeStatusResponseB*Z(git.wntrmute.dev/mc/mcp/gen/mcp/v1;mcpv1b\x06proto3"
"NodeStatus\x12\x19.mcp.v1.NodeStatusRequest\x1a\x1a.mcp.v1.NodeStatusResponse\x123\n" +
"\x04Logs\x12\x13.mcp.v1.LogsRequest\x1a\x14.mcp.v1.LogsResponse0\x01B*Z(git.wntrmute.dev/mc/mcp/gen/mcp/v1;mcpv1b\x06proto3"
var (
file_proto_mcp_v1_mcp_proto_rawDescOnce sync.Once
@@ -2401,7 +2541,7 @@ func file_proto_mcp_v1_mcp_proto_rawDescGZIP() []byte {
return file_proto_mcp_v1_mcp_proto_rawDescData
}
var file_proto_mcp_v1_mcp_proto_msgTypes = make([]protoimpl.MessageInfo, 39)
var file_proto_mcp_v1_mcp_proto_msgTypes = make([]protoimpl.MessageInfo, 41)
var file_proto_mcp_v1_mcp_proto_goTypes = []any{
(*RouteSpec)(nil), // 0: mcp.v1.RouteSpec
(*ComponentSpec)(nil), // 1: mcp.v1.ComponentSpec
@@ -2442,7 +2582,9 @@ var file_proto_mcp_v1_mcp_proto_goTypes = []any{
(*PurgeRequest)(nil), // 36: mcp.v1.PurgeRequest
(*PurgeResponse)(nil), // 37: mcp.v1.PurgeResponse
(*PurgeResult)(nil), // 38: mcp.v1.PurgeResult
(*timestamppb.Timestamp)(nil), // 39: google.protobuf.Timestamp
(*LogsRequest)(nil), // 39: mcp.v1.LogsRequest
(*LogsResponse)(nil), // 40: mcp.v1.LogsResponse
(*timestamppb.Timestamp)(nil), // 41: google.protobuf.Timestamp
}
var file_proto_mcp_v1_mcp_proto_depIdxs = []int32{
0, // 0: mcp.v1.ComponentSpec.routes:type_name -> mcp.v1.RouteSpec
@@ -2456,15 +2598,15 @@ var file_proto_mcp_v1_mcp_proto_depIdxs = []int32{
2, // 8: mcp.v1.SyncDesiredStateRequest.services:type_name -> mcp.v1.ServiceSpec
16, // 9: mcp.v1.SyncDesiredStateResponse.results:type_name -> mcp.v1.ServiceSyncResult
19, // 10: mcp.v1.ServiceInfo.components:type_name -> mcp.v1.ComponentInfo
39, // 11: mcp.v1.ComponentInfo.started:type_name -> google.protobuf.Timestamp
41, // 11: mcp.v1.ComponentInfo.started:type_name -> google.protobuf.Timestamp
18, // 12: mcp.v1.ListServicesResponse.services:type_name -> mcp.v1.ServiceInfo
39, // 13: mcp.v1.EventInfo.timestamp:type_name -> google.protobuf.Timestamp
41, // 13: mcp.v1.EventInfo.timestamp:type_name -> google.protobuf.Timestamp
18, // 14: mcp.v1.GetServiceStatusResponse.services:type_name -> mcp.v1.ServiceInfo
22, // 15: mcp.v1.GetServiceStatusResponse.drift:type_name -> mcp.v1.DriftInfo
23, // 16: mcp.v1.GetServiceStatusResponse.recent_events:type_name -> mcp.v1.EventInfo
18, // 17: mcp.v1.LiveCheckResponse.services:type_name -> mcp.v1.ServiceInfo
28, // 18: mcp.v1.AdoptContainersResponse.results:type_name -> mcp.v1.AdoptResult
39, // 19: mcp.v1.NodeStatusResponse.uptime_since:type_name -> google.protobuf.Timestamp
41, // 19: mcp.v1.NodeStatusResponse.uptime_since:type_name -> google.protobuf.Timestamp
38, // 20: mcp.v1.PurgeResponse.results:type_name -> mcp.v1.PurgeResult
3, // 21: mcp.v1.McpAgentService.Deploy:input_type -> mcp.v1.DeployRequest
12, // 22: mcp.v1.McpAgentService.UndeployService:input_type -> mcp.v1.UndeployServiceRequest
@@ -2480,22 +2622,24 @@ var file_proto_mcp_v1_mcp_proto_depIdxs = []int32{
30, // 32: mcp.v1.McpAgentService.PushFile:input_type -> mcp.v1.PushFileRequest
32, // 33: mcp.v1.McpAgentService.PullFile:input_type -> mcp.v1.PullFileRequest
34, // 34: mcp.v1.McpAgentService.NodeStatus:input_type -> mcp.v1.NodeStatusRequest
4, // 35: mcp.v1.McpAgentService.Deploy:output_type -> mcp.v1.DeployResponse
13, // 36: mcp.v1.McpAgentService.UndeployService:output_type -> mcp.v1.UndeployServiceResponse
7, // 37: mcp.v1.McpAgentService.StopService:output_type -> mcp.v1.StopServiceResponse
9, // 38: mcp.v1.McpAgentService.StartService:output_type -> mcp.v1.StartServiceResponse
11, // 39: mcp.v1.McpAgentService.RestartService:output_type -> mcp.v1.RestartServiceResponse
15, // 40: mcp.v1.McpAgentService.SyncDesiredState:output_type -> mcp.v1.SyncDesiredStateResponse
20, // 41: mcp.v1.McpAgentService.ListServices:output_type -> mcp.v1.ListServicesResponse
24, // 42: mcp.v1.McpAgentService.GetServiceStatus:output_type -> mcp.v1.GetServiceStatusResponse
26, // 43: mcp.v1.McpAgentService.LiveCheck:output_type -> mcp.v1.LiveCheckResponse
29, // 44: mcp.v1.McpAgentService.AdoptContainers:output_type -> mcp.v1.AdoptContainersResponse
37, // 45: mcp.v1.McpAgentService.PurgeComponent:output_type -> mcp.v1.PurgeResponse
31, // 46: mcp.v1.McpAgentService.PushFile:output_type -> mcp.v1.PushFileResponse
33, // 47: mcp.v1.McpAgentService.PullFile:output_type -> mcp.v1.PullFileResponse
35, // 48: mcp.v1.McpAgentService.NodeStatus:output_type -> mcp.v1.NodeStatusResponse
35, // [35:49] is the sub-list for method output_type
21, // [21:35] is the sub-list for method input_type
39, // 35: mcp.v1.McpAgentService.Logs:input_type -> mcp.v1.LogsRequest
4, // 36: mcp.v1.McpAgentService.Deploy:output_type -> mcp.v1.DeployResponse
13, // 37: mcp.v1.McpAgentService.UndeployService:output_type -> mcp.v1.UndeployServiceResponse
7, // 38: mcp.v1.McpAgentService.StopService:output_type -> mcp.v1.StopServiceResponse
9, // 39: mcp.v1.McpAgentService.StartService:output_type -> mcp.v1.StartServiceResponse
11, // 40: mcp.v1.McpAgentService.RestartService:output_type -> mcp.v1.RestartServiceResponse
15, // 41: mcp.v1.McpAgentService.SyncDesiredState:output_type -> mcp.v1.SyncDesiredStateResponse
20, // 42: mcp.v1.McpAgentService.ListServices:output_type -> mcp.v1.ListServicesResponse
24, // 43: mcp.v1.McpAgentService.GetServiceStatus:output_type -> mcp.v1.GetServiceStatusResponse
26, // 44: mcp.v1.McpAgentService.LiveCheck:output_type -> mcp.v1.LiveCheckResponse
29, // 45: mcp.v1.McpAgentService.AdoptContainers:output_type -> mcp.v1.AdoptContainersResponse
37, // 46: mcp.v1.McpAgentService.PurgeComponent:output_type -> mcp.v1.PurgeResponse
31, // 47: mcp.v1.McpAgentService.PushFile:output_type -> mcp.v1.PushFileResponse
33, // 48: mcp.v1.McpAgentService.PullFile:output_type -> mcp.v1.PullFileResponse
35, // 49: mcp.v1.McpAgentService.NodeStatus:output_type -> mcp.v1.NodeStatusResponse
40, // 50: mcp.v1.McpAgentService.Logs:output_type -> mcp.v1.LogsResponse
36, // [36:51] is the sub-list for method output_type
21, // [21:36] is the sub-list for method input_type
21, // [21:21] is the sub-list for extension type_name
21, // [21:21] is the sub-list for extension extendee
0, // [0:21] is the sub-list for field type_name
@@ -2512,7 +2656,7 @@ func file_proto_mcp_v1_mcp_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_mcp_v1_mcp_proto_rawDesc), len(file_proto_mcp_v1_mcp_proto_rawDesc)),
NumEnums: 0,
NumMessages: 39,
NumMessages: 41,
NumExtensions: 0,
NumServices: 1,
},

View File

@@ -33,6 +33,7 @@ const (
McpAgentService_PushFile_FullMethodName = "/mcp.v1.McpAgentService/PushFile"
McpAgentService_PullFile_FullMethodName = "/mcp.v1.McpAgentService/PullFile"
McpAgentService_NodeStatus_FullMethodName = "/mcp.v1.McpAgentService/NodeStatus"
McpAgentService_Logs_FullMethodName = "/mcp.v1.McpAgentService/Logs"
)
// McpAgentServiceClient is the client API for McpAgentService service.
@@ -60,6 +61,8 @@ type McpAgentServiceClient interface {
PullFile(ctx context.Context, in *PullFileRequest, opts ...grpc.CallOption) (*PullFileResponse, error)
// Node
NodeStatus(ctx context.Context, in *NodeStatusRequest, opts ...grpc.CallOption) (*NodeStatusResponse, error)
// Logs
Logs(ctx context.Context, in *LogsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[LogsResponse], error)
}
type mcpAgentServiceClient struct {
@@ -210,6 +213,25 @@ func (c *mcpAgentServiceClient) NodeStatus(ctx context.Context, in *NodeStatusRe
return out, nil
}
func (c *mcpAgentServiceClient) Logs(ctx context.Context, in *LogsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[LogsResponse], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &McpAgentService_ServiceDesc.Streams[0], McpAgentService_Logs_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[LogsRequest, LogsResponse]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type McpAgentService_LogsClient = grpc.ServerStreamingClient[LogsResponse]
// McpAgentServiceServer is the server API for McpAgentService service.
// All implementations must embed UnimplementedMcpAgentServiceServer
// for forward compatibility.
@@ -235,6 +257,8 @@ type McpAgentServiceServer interface {
PullFile(context.Context, *PullFileRequest) (*PullFileResponse, error)
// Node
NodeStatus(context.Context, *NodeStatusRequest) (*NodeStatusResponse, error)
// Logs
Logs(*LogsRequest, grpc.ServerStreamingServer[LogsResponse]) error
mustEmbedUnimplementedMcpAgentServiceServer()
}
@@ -287,6 +311,9 @@ func (UnimplementedMcpAgentServiceServer) PullFile(context.Context, *PullFileReq
func (UnimplementedMcpAgentServiceServer) NodeStatus(context.Context, *NodeStatusRequest) (*NodeStatusResponse, error) {
return nil, status.Error(codes.Unimplemented, "method NodeStatus not implemented")
}
func (UnimplementedMcpAgentServiceServer) Logs(*LogsRequest, grpc.ServerStreamingServer[LogsResponse]) error {
return status.Error(codes.Unimplemented, "method Logs not implemented")
}
func (UnimplementedMcpAgentServiceServer) mustEmbedUnimplementedMcpAgentServiceServer() {}
func (UnimplementedMcpAgentServiceServer) testEmbeddedByValue() {}
@@ -560,6 +587,17 @@ func _McpAgentService_NodeStatus_Handler(srv interface{}, ctx context.Context, d
return interceptor(ctx, in, info, handler)
}
func _McpAgentService_Logs_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(LogsRequest)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(McpAgentServiceServer).Logs(m, &grpc.GenericServerStream[LogsRequest, LogsResponse]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type McpAgentService_LogsServer = grpc.ServerStreamingServer[LogsResponse]
// McpAgentService_ServiceDesc is the grpc.ServiceDesc for McpAgentService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@@ -624,6 +662,12 @@ var McpAgentService_ServiceDesc = grpc.ServiceDesc{
Handler: _McpAgentService_NodeStatus_Handler,
},
},
Streams: []grpc.StreamDesc{},
Streams: []grpc.StreamDesc{
{
StreamName: "Logs",
Handler: _McpAgentService_Logs_Handler,
ServerStreams: true,
},
},
Metadata: "proto/mcp/v1/mcp.proto",
}