@@ -18,11 +18,11 @@ import (
1818 "github.com/spf13/cobra"
1919 "go.uber.org/zap"
2020
21- "mcpproxy-go/internal/cli/output"
22- "mcpproxy-go/internal/cliclient"
23- "mcpproxy-go/internal/config"
24- "mcpproxy-go/internal/logs"
25- "mcpproxy-go/internal/socket"
21+ "github.com/smart-mcp-proxy/ mcpproxy-go/internal/cli/output"
22+ "github.com/smart-mcp-proxy/ mcpproxy-go/internal/cliclient"
23+ "github.com/smart-mcp-proxy/ mcpproxy-go/internal/config"
24+ "github.com/smart-mcp-proxy/ mcpproxy-go/internal/logs"
25+ "github.com/smart-mcp-proxy/ mcpproxy-go/internal/socket"
2626)
2727
2828// Activity command flags
@@ -71,18 +71,26 @@ type ActivityFilter struct {
7171
7272// Validate validates the filter options
7373func (f * ActivityFilter ) Validate () error {
74- // Validate type
74+ // Validate type(s) - supports comma-separated values (Spec 024)
7575 if f .Type != "" {
76- validTypes := []string {"tool_call" , "policy_decision" , "quarantine_change" , "server_change" }
77- valid := false
78- for _ , t := range validTypes {
79- if f .Type == t {
80- valid = true
81- break
82- }
76+ validTypes := []string {
77+ "tool_call" , "policy_decision" , "quarantine_change" , "server_change" ,
78+ "system_start" , "system_stop" , "internal_tool_call" , "config_change" , // Spec 024: new types
8379 }
84- if ! valid {
85- return fmt .Errorf ("invalid type '%s': must be one of %v" , f .Type , validTypes )
80+ // Split by comma for multi-type support
81+ types := strings .Split (f .Type , "," )
82+ for _ , t := range types {
83+ t = strings .TrimSpace (t )
84+ valid := false
85+ for _ , vt := range validTypes {
86+ if t == vt {
87+ valid = true
88+ break
89+ }
90+ }
91+ if ! valid {
92+ return fmt .Errorf ("invalid type '%s': must be one of %v" , t , validTypes )
93+ }
8694 }
8795 }
8896
@@ -517,7 +525,7 @@ func init() {
517525 activityCmd .AddCommand (activityExportCmd )
518526
519527 // List command flags
520- activityListCmd .Flags ().StringVarP (& activityType , "type" , "t" , "" , "Filter by type: tool_call, policy_decision, quarantine_change, server_change" )
528+ activityListCmd .Flags ().StringVarP (& activityType , "type" , "t" , "" , "Filter by type (comma-separated for multiple) : tool_call, system_start, system_stop, internal_tool_call, config_change , policy_decision, quarantine_change, server_change" )
521529 activityListCmd .Flags ().StringVarP (& activityServer , "server" , "s" , "" , "Filter by server name" )
522530 activityListCmd .Flags ().StringVar (& activityTool , "tool" , "" , "Filter by tool name" )
523531 activityListCmd .Flags ().StringVar (& activityStatus , "status" , "" , "Filter by status: success, error, blocked" )
@@ -531,7 +539,7 @@ func init() {
531539 activityListCmd .Flags ().BoolVar (& activityNoIcons , "no-icons" , false , "Disable emoji icons in output (use text instead)" )
532540
533541 // Watch command flags
534- activityWatchCmd .Flags ().StringVarP (& activityType , "type" , "t" , "" , "Filter by type: tool_call, policy_decision" )
542+ activityWatchCmd .Flags ().StringVarP (& activityType , "type" , "t" , "" , "Filter by type (comma-separated) : tool_call, system_start, system_stop, internal_tool_call, config_change, policy_decision, quarantine_change, server_change " )
535543 activityWatchCmd .Flags ().StringVarP (& activityServer , "server" , "s" , "" , "Filter by server name" )
536544
537545 // Show command flags
@@ -547,7 +555,7 @@ func init() {
547555 activityExportCmd .Flags ().StringVarP (& activityExportFormat , "format" , "f" , "json" , "Export format: json, csv" )
548556 activityExportCmd .Flags ().BoolVar (& activityIncludeBodies , "include-bodies" , false , "Include full request/response bodies" )
549557 // Reuse list filter flags for export
550- activityExportCmd .Flags ().StringVarP (& activityType , "type" , "t" , "" , "Filter by type" )
558+ activityExportCmd .Flags ().StringVarP (& activityType , "type" , "t" , "" , "Filter by type (comma-separated): tool_call, system_start, system_stop, internal_tool_call, config_change, policy_decision, quarantine_change, server_change " )
551559 activityExportCmd .Flags ().StringVarP (& activityServer , "server" , "s" , "" , "Filter by server name" )
552560 activityExportCmd .Flags ().StringVar (& activityTool , "tool" , "" , "Filter by tool name" )
553561 activityExportCmd .Flags ().StringVar (& activityStatus , "status" , "" , "Filter by status" )
@@ -863,8 +871,9 @@ func watchActivityStream(ctx context.Context, sseURL string, outputFormat string
863871 eventData = strings .TrimPrefix (line , "data: " )
864872 case line == "" :
865873 // Empty line = event complete
866- // Only display completed events (started events have no status/duration)
867- if strings .HasPrefix (eventType , "activity." ) && strings .HasSuffix (eventType , ".completed" ) {
874+ // Display all activity events except .started (which have no meaningful status/duration)
875+ // Includes: .completed, policy_decision, system_start, system_stop, config_change
876+ if strings .HasPrefix (eventType , "activity." ) && ! strings .HasSuffix (eventType , ".started" ) {
868877 displayActivityEvent (eventType , eventData , outputFormat )
869878 }
870879 eventType , eventData = "" , ""
@@ -895,42 +904,81 @@ func displayActivityEvent(eventType, eventData, outputFormat string) {
895904 event = wrapper
896905 }
897906
907+ // Determine event category from eventType (e.g., "activity.tool_call.completed" -> "tool_call")
908+ parts := strings .Split (eventType , "." )
909+ eventCategory := ""
910+ if len (parts ) >= 2 {
911+ eventCategory = parts [1 ]
912+ }
913+
898914 // Apply client-side filters
899915 if activityServer != "" {
900- if server := getStringField (event , "server_name" ); server != activityServer {
916+ // For tool_call events, check server_name
917+ // For internal_tool_call events, check target_server
918+ server := getStringField (event , "server_name" )
919+ if server == "" {
920+ server = getStringField (event , "target_server" )
921+ }
922+ if server == "" {
923+ server = getStringField (event , "affected_entity" ) // for config_change
924+ }
925+ if server != activityServer {
901926 return
902927 }
903928 }
904929 if activityType != "" {
905- // Event type is like "activity.tool_call.completed", extract the middle part
906- parts := strings .Split (eventType , "." )
907- if len (parts ) >= 2 && parts [1 ] != activityType {
930+ if eventCategory != activityType {
931+ return
932+ }
933+ }
934+
935+ // Skip successful call_tool_* internal tool calls to avoid duplicates
936+ // These have a corresponding tool_call entry that shows the actual upstream call.
937+ // Failed call_tool_* calls are shown since they have no corresponding tool_call.
938+ if eventCategory == "internal_tool_call" {
939+ internalToolName := getStringField (event , "internal_tool_name" )
940+ status := getStringField (event , "status" )
941+ if status == "success" && strings .HasPrefix (internalToolName , "call_tool_" ) {
908942 return
909943 }
910944 }
911945
912- // Format for table output: [HH:MM:SS] [SRC] server:tool status duration
946+ // Format output based on event type
913947 timestamp := time .Now ().Format ("15:04:05" )
948+
949+ var line string
950+ switch eventCategory {
951+ case "tool_call" :
952+ line = formatToolCallEvent (event , timestamp )
953+ case "internal_tool_call" :
954+ line = formatInternalToolCallEvent (event , timestamp )
955+ case "policy_decision" :
956+ line = formatPolicyDecisionEvent (event , timestamp )
957+ case "system_start" :
958+ line = formatSystemStartEvent (event , timestamp )
959+ case "system_stop" :
960+ line = formatSystemStopEvent (event , timestamp )
961+ case "config_change" :
962+ line = formatConfigChangeEvent (event , timestamp )
963+ default :
964+ // Fallback for unknown event types
965+ line = fmt .Sprintf ("[%s] [?] %s" , timestamp , eventType )
966+ }
967+
968+ fmt .Println (line )
969+ }
970+
971+ // formatToolCallEvent formats a tool_call event for display
972+ func formatToolCallEvent (event map [string ]interface {}, timestamp string ) string {
914973 source := getStringField (event , "source" )
915974 server := getStringField (event , "server_name" )
916975 tool := getStringField (event , "tool_name" )
917976 status := getStringField (event , "status" )
918977 durationMs := getIntField (event , "duration_ms" )
919978 errMsg := getStringField (event , "error_message" )
920979
921- // Source indicator
922980 sourceIcon := formatSourceIndicator (source )
923-
924- // Status indicator
925- statusIcon := "?"
926- switch status {
927- case "success" :
928- statusIcon = "\u2713 " // checkmark
929- case "error" :
930- statusIcon = "\u2717 " // X
931- case "blocked" :
932- statusIcon = "\u2298 " // circle with slash
933- }
981+ statusIcon := formatStatusIcon (status )
934982
935983 line := fmt .Sprintf ("[%s] [%s] %s:%s %s %s" , timestamp , sourceIcon , server , tool , statusIcon , formatActivityDuration (int64 (durationMs )))
936984 if errMsg != "" {
@@ -939,8 +987,106 @@ func displayActivityEvent(eventType, eventData, outputFormat string) {
939987 if status == "blocked" {
940988 line += " BLOCKED"
941989 }
990+ return line
991+ }
942992
943- fmt .Println (line )
993+ // formatInternalToolCallEvent formats an internal_tool_call event for display
994+ func formatInternalToolCallEvent (event map [string ]interface {}, timestamp string ) string {
995+ internalTool := getStringField (event , "internal_tool_name" )
996+ targetServer := getStringField (event , "target_server" )
997+ targetTool := getStringField (event , "target_tool" )
998+ status := getStringField (event , "status" )
999+ durationMs := getIntField (event , "duration_ms" )
1000+ errMsg := getStringField (event , "error_message" )
1001+
1002+ statusIcon := formatStatusIcon (status )
1003+
1004+ // Format: [HH:MM:SS] [INT] internal_tool -> target_server:target_tool status duration
1005+ target := ""
1006+ if targetServer != "" && targetTool != "" {
1007+ target = fmt .Sprintf (" -> %s:%s" , targetServer , targetTool )
1008+ } else if targetServer != "" {
1009+ target = fmt .Sprintf (" -> %s" , targetServer )
1010+ }
1011+
1012+ line := fmt .Sprintf ("[%s] [INT] %s%s %s %s" , timestamp , internalTool , target , statusIcon , formatActivityDuration (int64 (durationMs )))
1013+ if errMsg != "" {
1014+ line += " " + errMsg
1015+ }
1016+ return line
1017+ }
1018+
1019+ // formatPolicyDecisionEvent formats a policy_decision event for display
1020+ func formatPolicyDecisionEvent (event map [string ]interface {}, timestamp string ) string {
1021+ server := getStringField (event , "server_name" )
1022+ tool := getStringField (event , "tool_name" )
1023+ decision := getStringField (event , "decision" )
1024+ reason := getStringField (event , "reason" )
1025+
1026+ statusIcon := "\u2298 " // circle with slash for blocked
1027+ if decision == "allowed" {
1028+ statusIcon = "\u2713 "
1029+ }
1030+
1031+ line := fmt .Sprintf ("[%s] [POL] %s:%s %s" , timestamp , server , tool , statusIcon )
1032+ if reason != "" {
1033+ line += " " + reason
1034+ }
1035+ return line
1036+ }
1037+
1038+ // formatSystemStartEvent formats a system_start event for display
1039+ func formatSystemStartEvent (event map [string ]interface {}, timestamp string ) string {
1040+ version := getStringField (event , "version" )
1041+ listenAddr := getStringField (event , "listen_address" )
1042+ startupMs := getIntField (event , "startup_duration_ms" )
1043+
1044+ return fmt .Sprintf ("[%s] [SYS] \u25B6 Started v%s on %s (%s)" , timestamp , version , listenAddr , formatActivityDuration (int64 (startupMs )))
1045+ }
1046+
1047+ // formatSystemStopEvent formats a system_stop event for display
1048+ func formatSystemStopEvent (event map [string ]interface {}, timestamp string ) string {
1049+ reason := getStringField (event , "reason" )
1050+ signal := getStringField (event , "signal" )
1051+ uptimeSec := getIntField (event , "uptime_seconds" )
1052+ errMsg := getStringField (event , "error_message" )
1053+
1054+ line := fmt .Sprintf ("[%s] [SYS] \u25A0 Stopped: %s" , timestamp , reason )
1055+ if signal != "" {
1056+ line += fmt .Sprintf (" (signal: %s)" , signal )
1057+ }
1058+ if uptimeSec > 0 {
1059+ line += fmt .Sprintf (" uptime: %ds" , uptimeSec )
1060+ }
1061+ if errMsg != "" {
1062+ line += " error: " + errMsg
1063+ }
1064+ return line
1065+ }
1066+
1067+ // formatConfigChangeEvent formats a config_change event for display
1068+ func formatConfigChangeEvent (event map [string ]interface {}, timestamp string ) string {
1069+ action := getStringField (event , "action" )
1070+ entity := getStringField (event , "affected_entity" )
1071+ source := getStringField (event , "source" )
1072+
1073+ sourceIcon := formatSourceIndicator (source )
1074+
1075+ return fmt .Sprintf ("[%s] [%s] \u2699 Config: %s %s" , timestamp , sourceIcon , action , entity )
1076+ }
1077+
1078+ // formatStatusIcon returns a status icon for the given status
1079+ func formatStatusIcon (status string ) string {
1080+ switch status {
1081+ case "success" :
1082+ return "\u2713 " // checkmark
1083+ case "error" :
1084+ return "\u2717 " // X
1085+ case "blocked" :
1086+ return "\u2298 " // circle with slash
1087+ default :
1088+ return "?"
1089+ }
9441090}
9451091
9461092// runActivityShow implements the activity show command
0 commit comments