diff --git a/CLAUDE.md b/CLAUDE.md index 797ed327..508815e2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -391,6 +391,8 @@ See `docs/prerelease-builds.md` for download instructions. - Go 1.24 (toolchain go1.24.10) + Cobra (CLI), Chi router (HTTP), Zap (logging), mark3labs/mcp-go (MCP protocol) (020-oauth-login-feedback) - Go 1.24 (toolchain go1.24.10) + Cobra (CLI), Chi router (HTTP), Zap (logging), google/uuid (ID generation) (021-request-id-logging) - BBolt database (`~/.mcpproxy/config.db`) - activity log extended with request_id field (021-request-id-logging) +- Go 1.24 (toolchain go1.24.10) + TypeScript 5.x / Vue 3.5 + Cobra CLI, Chi router, BBolt storage, Zap logging, mark3labs/mcp-go, Vue 3, Tailwind CSS, DaisyUI (024-expand-activity-log) +- BBolt database (`~/.mcpproxy/config.db`) - ActivityRecord model (024-expand-activity-log) ## Recent Changes - 001-update-version-display: Added Go 1.24 (toolchain go1.24.10) diff --git a/cmd/mcpproxy/activity_cmd.go b/cmd/mcpproxy/activity_cmd.go index 8fe4c85e..a5feaf6c 100644 --- a/cmd/mcpproxy/activity_cmd.go +++ b/cmd/mcpproxy/activity_cmd.go @@ -71,18 +71,26 @@ type ActivityFilter struct { // Validate validates the filter options func (f *ActivityFilter) Validate() error { - // Validate type + // Validate type(s) - supports comma-separated values (Spec 024) if f.Type != "" { - validTypes := []string{"tool_call", "policy_decision", "quarantine_change", "server_change"} - valid := false - for _, t := range validTypes { - if f.Type == t { - valid = true - break - } + validTypes := []string{ + "tool_call", "policy_decision", "quarantine_change", "server_change", + "system_start", "system_stop", "internal_tool_call", "config_change", // Spec 024: new types } - if !valid { - return fmt.Errorf("invalid type '%s': must be one of %v", f.Type, validTypes) + // Split by comma for multi-type support + types := strings.Split(f.Type, ",") + for _, t := range types { + t = strings.TrimSpace(t) + valid := false + for _, vt := range validTypes { + if t == vt { + valid = true + break + } + } + if !valid { + return fmt.Errorf("invalid type '%s': must be one of %v", t, validTypes) + } } } @@ -517,7 +525,7 @@ func init() { activityCmd.AddCommand(activityExportCmd) // List command flags - activityListCmd.Flags().StringVarP(&activityType, "type", "t", "", "Filter by type: tool_call, policy_decision, quarantine_change, server_change") + 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") activityListCmd.Flags().StringVarP(&activityServer, "server", "s", "", "Filter by server name") activityListCmd.Flags().StringVar(&activityTool, "tool", "", "Filter by tool name") activityListCmd.Flags().StringVar(&activityStatus, "status", "", "Filter by status: success, error, blocked") @@ -531,7 +539,7 @@ func init() { activityListCmd.Flags().BoolVar(&activityNoIcons, "no-icons", false, "Disable emoji icons in output (use text instead)") // Watch command flags - activityWatchCmd.Flags().StringVarP(&activityType, "type", "t", "", "Filter by type: tool_call, policy_decision") + 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") activityWatchCmd.Flags().StringVarP(&activityServer, "server", "s", "", "Filter by server name") // Show command flags @@ -547,7 +555,7 @@ func init() { activityExportCmd.Flags().StringVarP(&activityExportFormat, "format", "f", "json", "Export format: json, csv") activityExportCmd.Flags().BoolVar(&activityIncludeBodies, "include-bodies", false, "Include full request/response bodies") // Reuse list filter flags for export - activityExportCmd.Flags().StringVarP(&activityType, "type", "t", "", "Filter by type") + 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") activityExportCmd.Flags().StringVarP(&activityServer, "server", "s", "", "Filter by server name") activityExportCmd.Flags().StringVar(&activityTool, "tool", "", "Filter by tool name") activityExportCmd.Flags().StringVar(&activityStatus, "status", "", "Filter by status") @@ -863,8 +871,9 @@ func watchActivityStream(ctx context.Context, sseURL string, outputFormat string eventData = strings.TrimPrefix(line, "data: ") case line == "": // Empty line = event complete - // Only display completed events (started events have no status/duration) - if strings.HasPrefix(eventType, "activity.") && strings.HasSuffix(eventType, ".completed") { + // Display all activity events except .started (which have no meaningful status/duration) + // Includes: .completed, policy_decision, system_start, system_stop, config_change + if strings.HasPrefix(eventType, "activity.") && !strings.HasSuffix(eventType, ".started") { displayActivityEvent(eventType, eventData, outputFormat) } eventType, eventData = "", "" @@ -895,22 +904,72 @@ func displayActivityEvent(eventType, eventData, outputFormat string) { event = wrapper } + // Determine event category from eventType (e.g., "activity.tool_call.completed" -> "tool_call") + parts := strings.Split(eventType, ".") + eventCategory := "" + if len(parts) >= 2 { + eventCategory = parts[1] + } + // Apply client-side filters if activityServer != "" { - if server := getStringField(event, "server_name"); server != activityServer { + // For tool_call events, check server_name + // For internal_tool_call events, check target_server + server := getStringField(event, "server_name") + if server == "" { + server = getStringField(event, "target_server") + } + if server == "" { + server = getStringField(event, "affected_entity") // for config_change + } + if server != activityServer { return } } if activityType != "" { - // Event type is like "activity.tool_call.completed", extract the middle part - parts := strings.Split(eventType, ".") - if len(parts) >= 2 && parts[1] != activityType { + if eventCategory != activityType { + return + } + } + + // Skip successful call_tool_* internal tool calls to avoid duplicates + // These have a corresponding tool_call entry that shows the actual upstream call. + // Failed call_tool_* calls are shown since they have no corresponding tool_call. + if eventCategory == "internal_tool_call" { + internalToolName := getStringField(event, "internal_tool_name") + status := getStringField(event, "status") + if status == "success" && strings.HasPrefix(internalToolName, "call_tool_") { return } } - // Format for table output: [HH:MM:SS] [SRC] server:tool status duration + // Format output based on event type timestamp := time.Now().Format("15:04:05") + + var line string + switch eventCategory { + case "tool_call": + line = formatToolCallEvent(event, timestamp) + case "internal_tool_call": + line = formatInternalToolCallEvent(event, timestamp) + case "policy_decision": + line = formatPolicyDecisionEvent(event, timestamp) + case "system_start": + line = formatSystemStartEvent(event, timestamp) + case "system_stop": + line = formatSystemStopEvent(event, timestamp) + case "config_change": + line = formatConfigChangeEvent(event, timestamp) + default: + // Fallback for unknown event types + line = fmt.Sprintf("[%s] [?] %s", timestamp, eventType) + } + + fmt.Println(line) +} + +// formatToolCallEvent formats a tool_call event for display +func formatToolCallEvent(event map[string]interface{}, timestamp string) string { source := getStringField(event, "source") server := getStringField(event, "server_name") tool := getStringField(event, "tool_name") @@ -918,19 +977,8 @@ func displayActivityEvent(eventType, eventData, outputFormat string) { durationMs := getIntField(event, "duration_ms") errMsg := getStringField(event, "error_message") - // Source indicator sourceIcon := formatSourceIndicator(source) - - // Status indicator - statusIcon := "?" - switch status { - case "success": - statusIcon = "\u2713" // checkmark - case "error": - statusIcon = "\u2717" // X - case "blocked": - statusIcon = "\u2298" // circle with slash - } + statusIcon := formatStatusIcon(status) line := fmt.Sprintf("[%s] [%s] %s:%s %s %s", timestamp, sourceIcon, server, tool, statusIcon, formatActivityDuration(int64(durationMs))) if errMsg != "" { @@ -939,8 +987,106 @@ func displayActivityEvent(eventType, eventData, outputFormat string) { if status == "blocked" { line += " BLOCKED" } + return line +} - fmt.Println(line) +// formatInternalToolCallEvent formats an internal_tool_call event for display +func formatInternalToolCallEvent(event map[string]interface{}, timestamp string) string { + internalTool := getStringField(event, "internal_tool_name") + targetServer := getStringField(event, "target_server") + targetTool := getStringField(event, "target_tool") + status := getStringField(event, "status") + durationMs := getIntField(event, "duration_ms") + errMsg := getStringField(event, "error_message") + + statusIcon := formatStatusIcon(status) + + // Format: [HH:MM:SS] [INT] internal_tool -> target_server:target_tool status duration + target := "" + if targetServer != "" && targetTool != "" { + target = fmt.Sprintf(" -> %s:%s", targetServer, targetTool) + } else if targetServer != "" { + target = fmt.Sprintf(" -> %s", targetServer) + } + + line := fmt.Sprintf("[%s] [INT] %s%s %s %s", timestamp, internalTool, target, statusIcon, formatActivityDuration(int64(durationMs))) + if errMsg != "" { + line += " " + errMsg + } + return line +} + +// formatPolicyDecisionEvent formats a policy_decision event for display +func formatPolicyDecisionEvent(event map[string]interface{}, timestamp string) string { + server := getStringField(event, "server_name") + tool := getStringField(event, "tool_name") + decision := getStringField(event, "decision") + reason := getStringField(event, "reason") + + statusIcon := "\u2298" // circle with slash for blocked + if decision == "allowed" { + statusIcon = "\u2713" + } + + line := fmt.Sprintf("[%s] [POL] %s:%s %s", timestamp, server, tool, statusIcon) + if reason != "" { + line += " " + reason + } + return line +} + +// formatSystemStartEvent formats a system_start event for display +func formatSystemStartEvent(event map[string]interface{}, timestamp string) string { + version := getStringField(event, "version") + listenAddr := getStringField(event, "listen_address") + startupMs := getIntField(event, "startup_duration_ms") + + return fmt.Sprintf("[%s] [SYS] \u25B6 Started v%s on %s (%s)", timestamp, version, listenAddr, formatActivityDuration(int64(startupMs))) +} + +// formatSystemStopEvent formats a system_stop event for display +func formatSystemStopEvent(event map[string]interface{}, timestamp string) string { + reason := getStringField(event, "reason") + signal := getStringField(event, "signal") + uptimeSec := getIntField(event, "uptime_seconds") + errMsg := getStringField(event, "error_message") + + line := fmt.Sprintf("[%s] [SYS] \u25A0 Stopped: %s", timestamp, reason) + if signal != "" { + line += fmt.Sprintf(" (signal: %s)", signal) + } + if uptimeSec > 0 { + line += fmt.Sprintf(" uptime: %ds", uptimeSec) + } + if errMsg != "" { + line += " error: " + errMsg + } + return line +} + +// formatConfigChangeEvent formats a config_change event for display +func formatConfigChangeEvent(event map[string]interface{}, timestamp string) string { + action := getStringField(event, "action") + entity := getStringField(event, "affected_entity") + source := getStringField(event, "source") + + sourceIcon := formatSourceIndicator(source) + + return fmt.Sprintf("[%s] [%s] \u2699 Config: %s %s", timestamp, sourceIcon, action, entity) +} + +// formatStatusIcon returns a status icon for the given status +func formatStatusIcon(status string) string { + switch status { + case "success": + return "\u2713" // checkmark + case "error": + return "\u2717" // X + case "blocked": + return "\u2298" // circle with slash + default: + return "?" + } } // runActivityShow implements the activity show command diff --git a/cmd/mcpproxy/main.go b/cmd/mcpproxy/main.go index 4ef7f92a..fcbe6273 100644 --- a/cmd/mcpproxy/main.go +++ b/cmd/mcpproxy/main.go @@ -30,6 +30,7 @@ import ( "os" "os/signal" "strings" + "sync/atomic" "syscall" "time" @@ -554,6 +555,10 @@ func runServer(cmd *cobra.Command, _ []string) error { sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + // Spec 024: Track received signal for activity logging + var receivedSignal atomic.Value + receivedSignal.Store("") + // Setup signal handling for graceful shutdown with force quit on second signal logger.Info("Signal handler goroutine starting - waiting for SIGINT or SIGTERM") _ = logger.Sync() @@ -561,6 +566,7 @@ func runServer(cmd *cobra.Command, _ []string) error { logger.Info("Signal handler goroutine is running, waiting for signal on channel") _ = logger.Sync() sig := <-sigChan + receivedSignal.Store(sig.String()) // Spec 024: Store signal for activity logging logger.Info("Received signal, shutting down", zap.String("signal", sig.String())) _ = logger.Sync() // Flush logs immediately so we can see shutdown messages logger.Info("Press Ctrl+C again within 10 seconds to force quit") @@ -592,6 +598,8 @@ func runServer(cmd *cobra.Command, _ []string) error { // Wait for context to be cancelled <-ctx.Done() logger.Info("Shutting down server") + // Spec 024: Set shutdown info for activity logging + srv.SetShutdownInfo("signal", receivedSignal.Load().(string)) // Use Shutdown() instead of StopServer() to ensure proper container cleanup // Shutdown() calls runtime.Close() which triggers ShutdownAll() for Docker cleanup if err := srv.Shutdown(); err != nil { diff --git a/docs/cli/activity-commands.md b/docs/cli/activity-commands.md index 1b971323..8f93ed09 100644 --- a/docs/cli/activity-commands.md +++ b/docs/cli/activity-commands.md @@ -54,7 +54,7 @@ mcpproxy activity list [flags] | Flag | Short | Default | Description | |------|-------|---------|-------------| -| `--type` | `-t` | | Filter by type: `tool_call`, `policy_decision`, `quarantine_change`, `server_change` | +| `--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` | | `--server` | `-s` | | Filter by server name | | `--tool` | | | Filter by tool name | | `--status` | | | Filter by status: `success`, `error`, `blocked` | @@ -91,6 +91,18 @@ mcpproxy activity list --request-id a1b2c3d4-e5f6-7890-abcd-ef1234567890 # List activity as JSON mcpproxy activity list -o json +# List multiple event types (comma-separated) +mcpproxy activity list --type tool_call,config_change + +# List system lifecycle events +mcpproxy activity list --type system_start,system_stop + +# List internal tool calls (retrieve_tools, call_tool_*, upstream_servers, etc.) +mcpproxy activity list --type internal_tool_call + +# List configuration changes +mcpproxy activity list --type config_change + # List activity from today mcpproxy activity list --start-time "$(date -u +%Y-%m-%dT00:00:00Z)" @@ -163,7 +175,7 @@ mcpproxy activity watch [flags] | Flag | Short | Default | Description | |------|-------|---------|-------------| -| `--type` | `-t` | | Filter by type: `tool_call`, `policy_decision` | +| `--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` | | `--server` | `-s` | | Filter by server name | ### Examples @@ -175,6 +187,9 @@ mcpproxy activity watch # Watch only tool calls from github mcpproxy activity watch --type tool_call --server github +# Watch system and config events +mcpproxy activity watch --type system_start,system_stop,config_change + # Watch with JSON output (NDJSON) mcpproxy activity watch -o json @@ -205,6 +220,7 @@ Source indicators (`[MCP]`, `[CLI]`, `[API]`) show how the tool call was trigger - Automatically reconnects on connection loss (exponential backoff) - Exits cleanly on SIGINT (Ctrl+C) or SIGTERM - Buffers high-volume events to prevent terminal flooding +- **Filters out successful `call_tool_*` internal tool calls** to avoid duplicates (they have corresponding `tool_call` entries) ### Exit Codes @@ -425,6 +441,12 @@ mcpproxy activity export --format csv | gzip > activity.csv.gz # Export errors only mcpproxy activity export --status error --output errors.jsonl + +# Export specific event types +mcpproxy activity export --type tool_call,internal_tool_call --output tool-calls.jsonl + +# Export config changes for audit +mcpproxy activity export --type config_change --output config-audit.jsonl ``` ### Output (JSON - JSON Lines) @@ -540,6 +562,44 @@ Hint: Use 'mcpproxy activity list' to find valid activity IDs --- +## Event Types Reference + +The activity log captures the following event types: + +| Type | Description | +|------|-------------| +| `tool_call` | Every tool call made through MCPProxy to upstream servers | +| `system_start` | MCPProxy server startup events | +| `system_stop` | MCPProxy server shutdown events | +| `internal_tool_call` | Internal proxy tool calls (`retrieve_tools`, `call_tool_*`, `code_execution`, `upstream_servers`, etc.) | +| `config_change` | Configuration changes (server added/removed/updated) | +| `policy_decision` | Tool calls blocked by policy rules | +| `quarantine_change` | Server quarantine/unquarantine events | +| `server_change` | Server enable/disable/restart events | + +:::note Duplicate Filtering for call_tool_* +By default, **successful** `call_tool_*` internal tool calls are filtered out from `activity list`, `activity watch`, and the Web UI because they appear as duplicates alongside their corresponding upstream `tool_call` entries. **Failed** `call_tool_*` calls are always shown since they have no corresponding tool call entry. + +To include all internal tool calls in API responses, use `include_call_tool=true` query parameter. +::: + +### Multi-Type Filtering + +You can filter by multiple types using comma-separated values: + +```bash +# Filter by multiple types +mcpproxy activity list --type tool_call,internal_tool_call + +# System lifecycle events +mcpproxy activity list --type system_start,system_stop + +# All config-related events +mcpproxy activity list --type config_change,quarantine_change,server_change +``` + +--- + ## Tips - Use `--json` output for piping to `jq` for complex filtering diff --git a/docs/features/activity-log.md b/docs/features/activity-log.md index f914d65b..2d3ced72 100644 --- a/docs/features/activity-log.md +++ b/docs/features/activity-log.md @@ -18,10 +18,86 @@ The activity log captures: | Event Type | Description | |------------|-------------| | `tool_call` | Every tool call made through MCPProxy | +| `system_start` | MCPProxy server startup events | +| `system_stop` | MCPProxy server shutdown events | +| `internal_tool_call` | Internal proxy tool calls (retrieve_tools, call_tool_*, code_execution, etc.) | +| `config_change` | Configuration changes (server added/removed/updated) | | `policy_decision` | Tool calls blocked by policy rules | | `quarantine_change` | Server quarantine/unquarantine events | | `server_change` | Server enable/disable/restart events | +### System Lifecycle Events + +System lifecycle events track when MCPProxy starts and stops: + +```json +{ + "id": "01JFXYZ123DEF", + "type": "system_start", + "status": "success", + "timestamp": "2025-01-15T10:00:00Z", + "metadata": { + "version": "v0.5.0", + "listen_address": "127.0.0.1:8080", + "startup_duration_ms": 150, + "config_path": "/Users/user/.mcpproxy/mcp_config.json" + } +} +``` + +### Internal Tool Call Events + +Internal tool calls log when internal proxy tools are used: + +```json +{ + "id": "01JFXYZ123GHI", + "type": "internal_tool_call", + "status": "success", + "duration_ms": 45, + "timestamp": "2025-01-15T10:05:00Z", + "metadata": { + "internal_tool_name": "call_tool_read", + "target_server": "github-server", + "target_tool": "get_user", + "tool_variant": "call_tool_read", + "intent": { + "operation_type": "read", + "data_sensitivity": "public" + } + } +} +``` + +:::note Duplicate Filtering +By default, **successful** `call_tool_*` internal tool calls (`call_tool_read`, `call_tool_write`, `call_tool_destructive`) are excluded from activity listings because they appear as duplicates alongside their corresponding upstream `tool_call` entries. **Failed** `call_tool_*` calls are always shown since they have no corresponding upstream tool call entry. + +To include all internal tool calls including successful `call_tool_*`, use `include_call_tool=true` in the API query parameter. +::: + +### Config Change Events + +Configuration changes are logged for audit trails: + +```json +{ + "id": "01JFXYZ123JKL", + "type": "config_change", + "server_name": "github-server", + "status": "success", + "timestamp": "2025-01-15T10:10:00Z", + "metadata": { + "action": "server_added", + "affected_entity": "github-server", + "source": "mcp", + "new_values": { + "name": "github-server", + "url": "https://api.github.com/mcp" + } + } +} +``` + ### Tool Call Records Each tool call record includes: @@ -151,7 +227,7 @@ GET /api/v1/activity | Parameter | Type | Description | |-----------|------|-------------| -| `type` | string | Filter by type: `tool_call`, `policy_decision`, `quarantine_change`, `server_change` | +| `type` | string | Filter by type (comma-separated for multiple): `tool_call`, `system_start`, `system_stop`, `internal_tool_call`, `config_change`, `policy_decision`, `quarantine_change`, `server_change` | | `server` | string | Filter by server name | | `tool` | string | Filter by tool name | | `session_id` | string | Filter by MCP session ID | @@ -160,6 +236,7 @@ GET /api/v1/activity | `end_time` | string | Filter before this time (RFC3339) | | `limit` | integer | Max records (1-100, default: 50) | | `offset` | integer | Pagination offset (default: 0) | +| `include_call_tool` | boolean | Include successful `call_tool_*` internal tool calls (default: false). By default, successful `call_tool_*` are excluded because they appear as duplicates alongside their upstream `tool_call` entries. Failed `call_tool_*` are always shown. | **Example:** @@ -167,6 +244,12 @@ GET /api/v1/activity # List recent tool calls curl -H "X-API-Key: $KEY" "http://127.0.0.1:8080/api/v1/activity?type=tool_call&limit=10" +# Filter by multiple types (comma-separated) +curl -H "X-API-Key: $KEY" "http://127.0.0.1:8080/api/v1/activity?type=tool_call,internal_tool_call,config_change" + +# List system lifecycle events +curl -H "X-API-Key: $KEY" "http://127.0.0.1:8080/api/v1/activity?type=system_start,system_stop" + # Filter by server curl -H "X-API-Key: $KEY" "http://127.0.0.1:8080/api/v1/activity?server=github-server" diff --git a/docs/web-ui/activity-log.md b/docs/web-ui/activity-log.md index da57a4b8..06c3a868 100644 --- a/docs/web-ui/activity-log.md +++ b/docs/web-ui/activity-log.md @@ -10,22 +10,29 @@ Access the Activity Log by navigating to `/ui/activity` in the MCPProxy web inte ### Activity Table -The main view displays activities in a table with the following columns: +The main view displays activities in a sortable table with the following columns: -| Column | Description | -|--------|-------------| -| Time | Timestamp with relative time display (e.g., "5m ago") | -| Type | Activity type with icon indicator | -| Server | Link to the server that generated the activity | -| Details | Tool name or action description | -| Status | Color-coded badge (green=success, red=error, orange=blocked) | -| Duration | Execution time in ms or seconds | +| Column | Sortable | Description | +|--------|----------|-------------| +| Time | Yes | Timestamp with relative time display (e.g., "5m ago"). Default sort: newest first | +| Type | Yes | Activity type with icon indicator | +| Server | Yes | Link to the server that generated the activity | +| Details | No | Tool name or action description | +| Intent | No | Operation type badge (read/write/destructive) with tooltip showing full intent details | +| Status | Yes | Color-coded badge (green=success, red=error, orange=blocked) | +| Duration | Yes | Execution time in ms or seconds | + +**Sorting**: Click any sortable column header to sort. Click again to toggle between ascending/descending. The current sort column and direction are indicated with an arrow. ### Activity Types | Type | Icon | Description | |------|------|-------------| -| Tool Call | 🔧 | MCP tool invocations | +| Tool Call | 🔧 | MCP tool invocations to upstream servers | +| System Start | 🚀 | MCPProxy server startup events | +| System Stop | 🛑 | MCPProxy server shutdown events | +| Internal Tool Call | ⚙️ | Internal proxy tool calls (`retrieve_tools`, `call_tool_*`, `code_execution`, `upstream_servers`, etc.) | +| Config Change | ⚡ | Configuration changes (server added/removed/updated) | | Policy Decision | 🛡️ | Security policy evaluations | | Quarantine Change | ⚠️ | Server quarantine status changes | | Server Change | 🔄 | Server enable/disable/restart events | @@ -40,12 +47,13 @@ Activities appear automatically via Server-Sent Events (SSE): ### Filtering Filter activities by: -- **Type**: Tool Call, Policy Decision, Quarantine Change, Server Change +- **Type**: Multi-select dropdown with checkboxes. Select one or more types to filter (uses OR logic between selected types): + - Tool Call, System Start, System Stop, Internal Tool Call, Config Change, Policy Decision, Quarantine Change, Server Change - **Server**: Dynamically populated from activity data - **Status**: Success, Error, Blocked - **Date Range**: From/To datetime pickers to filter by time period -Filters combine with AND logic. Active filters are displayed as badges below the filter controls. +Type filters combine with OR logic (show any selected type). Other filters combine with AND logic. Active filters are displayed as badges below the filter controls. ### Activity Details @@ -102,9 +110,10 @@ The Activity Log uses these REST API endpoints: | `GET /api/v1/activity/export` | Export activities (JSON/CSV) | Query parameters for filtering: -- `type`: Filter by activity type +- `type`: Filter by activity type (comma-separated for multiple, e.g., `?type=tool_call,config_change`) - `server`: Filter by server name - `status`: Filter by status +- `intent_type`: Filter by intent operation type (`read`, `write`, `destructive`) - `limit`: Maximum records to return - `offset`: Pagination offset diff --git a/frontend/src/components/SidebarNav.vue b/frontend/src/components/SidebarNav.vue index fcad6ccb..1acd36a9 100644 --- a/frontend/src/components/SidebarNav.vue +++ b/frontend/src/components/SidebarNav.vue @@ -76,9 +76,7 @@ const menuItems = [ { name: 'Servers', path: '/servers' }, { name: 'Secrets', path: '/secrets' }, { name: 'Search', path: '/search' }, - { name: 'Tool Call History', path: '/tool-calls' }, - // TODO: Re-enable in next release - // { name: 'Activity Log', path: '/activity' }, + { name: 'Activity Log', path: '/activity' }, { name: 'Repositories', path: '/repositories' }, { name: 'Configuration', path: '/settings' }, ] diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 7691045f..9df9190a 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -61,14 +61,6 @@ const router = createRouter({ title: 'Secrets', }, }, - { - path: '/tool-calls', - name: 'tool-calls', - component: () => import('@/views/ToolCalls.vue'), - meta: { - title: 'Tool Call History', - }, - }, { path: '/sessions', name: 'sessions', diff --git a/frontend/src/stores/system.ts b/frontend/src/stores/system.ts index e22f7824..b6030dca 100644 --- a/frontend/src/stores/system.ts +++ b/frontend/src/stores/system.ts @@ -208,6 +208,55 @@ export const useSystemStore = defineStore('system', () => { } }) + // Listen for internal tool call events (Spec 024) + es.addEventListener('activity.internal_tool_call.completed', (event) => { + try { + const data = JSON.parse(event.data) + console.log('SSE activity.internal_tool_call.completed event received:', data) + const payload = data.payload || data + window.dispatchEvent(new CustomEvent('mcpproxy:activity-completed', { detail: payload })) + } catch (error) { + console.error('Failed to parse SSE activity.internal_tool_call.completed event:', error) + } + }) + + // Listen for system lifecycle events (Spec 024) + // Note: Backend sends "activity.system.start" (with dots, not underscores) + es.addEventListener('activity.system.start', (event) => { + try { + const data = JSON.parse(event.data) + console.log('SSE activity.system_start event received:', data) + const payload = data.payload || data + window.dispatchEvent(new CustomEvent('mcpproxy:activity', { detail: payload })) + } catch (error) { + console.error('Failed to parse SSE activity.system_start event:', error) + } + }) + + // Note: Backend sends "activity.system.stop" (with dots, not underscores) + es.addEventListener('activity.system.stop', (event) => { + try { + const data = JSON.parse(event.data) + console.log('SSE activity.system_stop event received:', data) + const payload = data.payload || data + window.dispatchEvent(new CustomEvent('mcpproxy:activity', { detail: payload })) + } catch (error) { + console.error('Failed to parse SSE activity.system_stop event:', error) + } + }) + + // Listen for config change events (Spec 024) + es.addEventListener('activity.config_change', (event) => { + try { + const data = JSON.parse(event.data) + console.log('SSE activity.config_change event received:', data) + const payload = data.payload || data + window.dispatchEvent(new CustomEvent('mcpproxy:activity', { detail: payload })) + } catch (error) { + console.error('Failed to parse SSE activity.config_change event:', error) + } + }) + es.onerror = (event) => { connected.value = false console.error('EventSource error occurred:', event) diff --git a/frontend/src/views/Activity.vue b/frontend/src/views/Activity.vue index f1e82007..664e63c9 100644 --- a/frontend/src/views/Activity.vue +++ b/frontend/src/views/Activity.vue @@ -54,18 +54,49 @@
- -
+ +
- +
@@ -94,6 +125,19 @@
+ +
+ + +
+