- | Time |
- Type |
- Server |
+
+ Time {{ getSortIndicator('timestamp') }}
+ |
+
+ Type {{ getSortIndicator('type') }}
+ |
+
+ Server {{ getSortIndicator('server_name') }}
+ |
Details |
- Status |
- Duration |
+ Intent |
+
+ Status {{ getSortIndicator('status') }}
+ |
+
+ Duration {{ getSortIndicator('duration_ms') }}
+ |
|
@@ -235,6 +301,20 @@
-
+
+
+
+
+ {{ getIntentIcon(activity.metadata.intent.operation_type) }}
+ {{ activity.metadata.intent.operation_type }}
+
+
+ -
+ |
- Showing {{ (currentPage - 1) * pageSize + 1 }}-{{ Math.min(currentPage * pageSize, filteredActivities.length) }} of {{ filteredActivities.length }}
+ Showing {{ (currentPage - 1) * pageSize + 1 }}-{{ Math.min(currentPage * pageSize, sortedActivities.length) }} of {{ sortedActivities.length }}
+
+
+
+
+ Policy Decision
+
+
+
+
+ Decision:
+ {{ selectedActivity.metadata?.decision || selectedActivity.status || 'Blocked' }}
+
+
+ Reason:
+ {{ selectedActivity.metadata.reason }}
+
+
+ Policy Rule:
+ {{ selectedActivity.metadata.policy_rule }}
+
+
+ Tool call was blocked by security policy
+
+
+
+
+
@@ -419,6 +528,15 @@
+
+
+
+
+ Additional Details
+ JSON
+
+
+
@@ -428,11 +546,13 @@
-
-
\ No newline at end of file
diff --git a/internal/httpapi/activity.go b/internal/httpapi/activity.go
index 3217bca9..aa3fdc13 100644
--- a/internal/httpapi/activity.go
+++ b/internal/httpapi/activity.go
@@ -19,9 +19,9 @@ func parseActivityFilters(r *http.Request) storage.ActivityFilter {
filter := storage.DefaultActivityFilter()
q := r.URL.Query()
- // Type filter
+ // Type filter (Spec 024: supports comma-separated multiple types)
if typeStr := q.Get("type"); typeStr != "" {
- filter.Type = typeStr
+ filter.Types = strings.Split(typeStr, ",")
}
// Server filter
@@ -80,6 +80,12 @@ func parseActivityFilters(r *http.Request) storage.ActivityFilter {
filter.RequestID = requestID
}
+ // Include call_tool_* internal tool calls (default: exclude successful ones)
+ // Set include_call_tool=true to show all internal tool calls including successful call_tool_*
+ if q.Get("include_call_tool") == "true" {
+ filter.ExcludeCallToolSuccess = false
+ }
+
filter.Validate()
return filter
}
@@ -90,13 +96,14 @@ func parseActivityFilters(r *http.Request) storage.ActivityFilter {
// @Tags Activity
// @Accept json
// @Produce json
-// @Param type query string false "Filter by activity type" Enums(tool_call, policy_decision, quarantine_change, server_change)
+// @Param type query string false "Filter by activity type(s), comma-separated for multiple (Spec 024)" Enums(tool_call, policy_decision, quarantine_change, server_change, system_start, system_stop, internal_tool_call, config_change)
// @Param server query string false "Filter by server name"
// @Param tool query string false "Filter by tool name"
// @Param session_id query string false "Filter by MCP session ID"
// @Param status query string false "Filter by status" Enums(success, error, blocked)
// @Param intent_type query string false "Filter by intent operation type (Spec 018)" Enums(read, write, destructive)
// @Param request_id query string false "Filter by HTTP request ID for log correlation (Spec 021)"
+// @Param include_call_tool query bool false "Include successful call_tool_* internal tool calls (default: false, excluded to avoid duplicates)"
// @Param start_time query string false "Filter activities after this time (RFC3339)"
// @Param end_time query string false "Filter activities before this time (RFC3339)"
// @Param limit query int false "Maximum records to return (1-100, default 50)"
diff --git a/internal/httpapi/activity_test.go b/internal/httpapi/activity_test.go
index 7a723368..bddea2e7 100644
--- a/internal/httpapi/activity_test.go
+++ b/internal/httpapi/activity_test.go
@@ -29,10 +29,28 @@ func TestParseActivityFilters(t *testing.T) {
},
},
{
- name: "type filter",
+ name: "single type filter",
query: "type=tool_call",
expected: storage.ActivityFilter{
- Type: "tool_call",
+ Types: []string{"tool_call"},
+ Limit: 50,
+ Offset: 0,
+ },
+ },
+ {
+ name: "multiple types filter (Spec 024)",
+ query: "type=tool_call,policy_decision",
+ expected: storage.ActivityFilter{
+ Types: []string{"tool_call", "policy_decision"},
+ Limit: 50,
+ Offset: 0,
+ },
+ },
+ {
+ name: "all new event types (Spec 024)",
+ query: "type=system_start,system_stop,internal_tool_call,config_change",
+ expected: storage.ActivityFilter{
+ Types: []string{"system_start", "system_stop", "internal_tool_call", "config_change"},
Limit: 50,
Offset: 0,
},
@@ -90,10 +108,10 @@ func TestParseActivityFilters(t *testing.T) {
},
},
{
- name: "multiple filters",
+ name: "multiple filters with types",
query: "type=tool_call&server=github&status=success&limit=20",
expected: storage.ActivityFilter{
- Type: "tool_call",
+ Types: []string{"tool_call"},
Server: "github",
Status: "success",
Limit: 20,
@@ -107,7 +125,7 @@ func TestParseActivityFilters(t *testing.T) {
req := httptest.NewRequest("GET", "/api/v1/activity?"+tt.query, nil)
filter := parseActivityFilters(req)
- assert.Equal(t, tt.expected.Type, filter.Type)
+ assert.Equal(t, tt.expected.Types, filter.Types)
assert.Equal(t, tt.expected.Server, filter.Server)
assert.Equal(t, tt.expected.Tool, filter.Tool)
assert.Equal(t, tt.expected.SessionID, filter.SessionID)
diff --git a/internal/runtime/activity_service.go b/internal/runtime/activity_service.go
index 00d627cd..e3e4c1a3 100644
--- a/internal/runtime/activity_service.go
+++ b/internal/runtime/activity_service.go
@@ -2,6 +2,7 @@ package runtime
import (
"context"
+ "encoding/json"
"time"
"go.uber.org/zap"
@@ -165,6 +166,15 @@ func (s *ActivityService) handleEvent(evt Event) {
zap.String("tool_name", getStringPayload(evt.Payload, "tool_name")),
zap.String("session_id", getStringPayload(evt.Payload, "session_id")),
zap.String("request_id", getStringPayload(evt.Payload, "request_id")))
+ // Spec 024: System lifecycle events
+ case EventTypeActivitySystemStart:
+ s.handleSystemStart(evt)
+ case EventTypeActivitySystemStop:
+ s.handleSystemStop(evt)
+ case EventTypeActivityInternalToolCall:
+ s.handleInternalToolCall(evt)
+ case EventTypeActivityConfigChange:
+ s.handleConfigChange(evt)
default:
// Ignore other event types
}
@@ -179,6 +189,7 @@ func (s *ActivityService) handleToolCallCompleted(evt Event) {
source := getStringPayload(evt.Payload, "source")
status := getStringPayload(evt.Payload, "status")
errorMsg := getStringPayload(evt.Payload, "error_message")
+ arguments := getMapPayload(evt.Payload, "arguments")
response := getStringPayload(evt.Payload, "response")
responseTruncated := getBoolPayload(evt.Payload, "response_truncated")
durationMs := getInt64Payload(evt.Payload, "duration_ms")
@@ -209,6 +220,7 @@ func (s *ActivityService) handleToolCallCompleted(evt Event) {
Source: activitySource,
ServerName: serverName,
ToolName: toolName,
+ Arguments: arguments,
Response: response,
ResponseTruncated: responseTruncated,
Status: status,
@@ -293,6 +305,199 @@ func (s *ActivityService) handleQuarantineChange(evt Event) {
}
}
+// handleSystemStart persists a system start event (Spec 024).
+func (s *ActivityService) handleSystemStart(evt Event) {
+ version := getStringPayload(evt.Payload, "version")
+ listenAddress := getStringPayload(evt.Payload, "listen_address")
+ startupDurationMs := getInt64Payload(evt.Payload, "startup_duration_ms")
+ configPath := getStringPayload(evt.Payload, "config_path")
+
+ record := &storage.ActivityRecord{
+ Type: storage.ActivityTypeSystemStart,
+ Source: storage.ActivitySourceAPI, // System events come from the API server
+ Status: "success",
+ Metadata: map[string]interface{}{
+ "version": version,
+ "listen_address": listenAddress,
+ "startup_duration_ms": startupDurationMs,
+ "config_path": configPath,
+ },
+ Timestamp: evt.Timestamp,
+ }
+
+ if err := s.storage.SaveActivity(record); err != nil {
+ s.logger.Error("Failed to save system start activity",
+ zap.Error(err),
+ zap.String("version", version))
+ } else {
+ s.logger.Info("System start activity recorded",
+ zap.String("id", record.ID),
+ zap.String("version", version),
+ zap.Int64("startup_duration_ms", startupDurationMs))
+ }
+}
+
+// handleSystemStop persists a system stop event (Spec 024).
+func (s *ActivityService) handleSystemStop(evt Event) {
+ reason := getStringPayload(evt.Payload, "reason")
+ signal := getStringPayload(evt.Payload, "signal")
+ uptimeSeconds := getInt64Payload(evt.Payload, "uptime_seconds")
+ errorMsg := getStringPayload(evt.Payload, "error_message")
+
+ status := "success"
+ if errorMsg != "" {
+ status = "error"
+ }
+
+ record := &storage.ActivityRecord{
+ Type: storage.ActivityTypeSystemStop,
+ Source: storage.ActivitySourceAPI,
+ Status: status,
+ ErrorMessage: errorMsg,
+ Metadata: map[string]interface{}{
+ "reason": reason,
+ "signal": signal,
+ "uptime_seconds": uptimeSeconds,
+ },
+ Timestamp: evt.Timestamp,
+ }
+
+ if err := s.storage.SaveActivity(record); err != nil {
+ s.logger.Error("Failed to save system stop activity",
+ zap.Error(err),
+ zap.String("reason", reason))
+ } else {
+ s.logger.Info("System stop activity recorded",
+ zap.String("id", record.ID),
+ zap.String("reason", reason),
+ zap.Int64("uptime_seconds", uptimeSeconds))
+ }
+}
+
+// handleInternalToolCall persists an internal tool call event (Spec 024).
+func (s *ActivityService) handleInternalToolCall(evt Event) {
+ internalToolName := getStringPayload(evt.Payload, "internal_tool_name")
+ targetServer := getStringPayload(evt.Payload, "target_server")
+ targetTool := getStringPayload(evt.Payload, "target_tool")
+ toolVariant := getStringPayload(evt.Payload, "tool_variant")
+ sessionID := getStringPayload(evt.Payload, "session_id")
+ requestID := getStringPayload(evt.Payload, "request_id")
+ status := getStringPayload(evt.Payload, "status")
+ errorMsg := getStringPayload(evt.Payload, "error_message")
+ durationMs := getInt64Payload(evt.Payload, "duration_ms")
+ intent := getMapPayload(evt.Payload, "intent")
+ arguments := getMapPayload(evt.Payload, "arguments")
+
+ // Extract response - can be various types, convert to string
+ var responseStr string
+ if resp := evt.Payload["response"]; resp != nil {
+ switch r := resp.(type) {
+ case string:
+ responseStr = r
+ default:
+ // Convert to JSON for other types
+ if jsonBytes, err := json.Marshal(r); err == nil {
+ responseStr = string(jsonBytes)
+ }
+ }
+ }
+
+ metadata := map[string]interface{}{
+ "internal_tool_name": internalToolName,
+ }
+ if targetServer != "" {
+ metadata["target_server"] = targetServer
+ }
+ if targetTool != "" {
+ metadata["target_tool"] = targetTool
+ }
+ if toolVariant != "" {
+ metadata["tool_variant"] = toolVariant
+ }
+ if intent != nil {
+ metadata["intent"] = intent
+ }
+
+ record := &storage.ActivityRecord{
+ Type: storage.ActivityTypeInternalToolCall,
+ Source: storage.ActivitySourceMCP,
+ ToolName: internalToolName,
+ ServerName: targetServer,
+ Arguments: arguments,
+ Response: responseStr,
+ Status: status,
+ ErrorMessage: errorMsg,
+ DurationMs: durationMs,
+ Metadata: metadata,
+ Timestamp: evt.Timestamp,
+ SessionID: sessionID,
+ RequestID: requestID,
+ }
+
+ if err := s.storage.SaveActivity(record); err != nil {
+ s.logger.Error("Failed to save internal tool call activity",
+ zap.Error(err),
+ zap.String("internal_tool_name", internalToolName))
+ } else {
+ s.logger.Debug("Internal tool call activity recorded",
+ zap.String("id", record.ID),
+ zap.String("internal_tool_name", internalToolName),
+ zap.String("status", status))
+ }
+}
+
+// handleConfigChange persists a config change event (Spec 024).
+func (s *ActivityService) handleConfigChange(evt Event) {
+ action := getStringPayload(evt.Payload, "action")
+ affectedEntity := getStringPayload(evt.Payload, "affected_entity")
+ source := getStringPayload(evt.Payload, "source")
+
+ var activitySource storage.ActivitySource
+ switch source {
+ case "cli":
+ activitySource = storage.ActivitySourceCLI
+ case "mcp":
+ activitySource = storage.ActivitySourceMCP
+ default:
+ activitySource = storage.ActivitySourceAPI
+ }
+
+ metadata := map[string]interface{}{
+ "action": action,
+ "affected_entity": affectedEntity,
+ }
+ if changedFields := getSlicePayload(evt.Payload, "changed_fields"); len(changedFields) > 0 {
+ metadata["changed_fields"] = changedFields
+ }
+ if prevValues := getMapPayload(evt.Payload, "previous_values"); prevValues != nil {
+ metadata["previous_values"] = prevValues
+ }
+ if newValues := getMapPayload(evt.Payload, "new_values"); newValues != nil {
+ metadata["new_values"] = newValues
+ }
+
+ record := &storage.ActivityRecord{
+ Type: storage.ActivityTypeConfigChange,
+ Source: activitySource,
+ ServerName: affectedEntity,
+ Status: "success",
+ Metadata: metadata,
+ Timestamp: evt.Timestamp,
+ }
+
+ if err := s.storage.SaveActivity(record); err != nil {
+ s.logger.Error("Failed to save config change activity",
+ zap.Error(err),
+ zap.String("action", action),
+ zap.String("affected_entity", affectedEntity))
+ } else {
+ s.logger.Info("Config change activity recorded",
+ zap.String("id", record.ID),
+ zap.String("action", action),
+ zap.String("affected_entity", affectedEntity))
+ }
+}
+
// Helper functions to extract payload values safely
func getStringPayload(payload map[string]any, key string) string {
@@ -340,3 +545,31 @@ func getMapPayload(payload map[string]any, key string) map[string]interface{} {
return nil
}
+func getSlicePayload(payload map[string]any, key string) []string {
+ if v, ok := payload[key]; ok {
+ if s, ok := v.([]string); ok {
+ return s
+ }
+ // Also handle []interface{} and convert to []string
+ if arr, ok := v.([]interface{}); ok {
+ result := make([]string, 0, len(arr))
+ for _, item := range arr {
+ if str, ok := item.(string); ok {
+ result = append(result, str)
+ }
+ }
+ return result
+ }
+ if arr, ok := v.([]any); ok {
+ result := make([]string, 0, len(arr))
+ for _, item := range arr {
+ if str, ok := item.(string); ok {
+ result = append(result, str)
+ }
+ }
+ return result
+ }
+ }
+ return nil
+}
+
diff --git a/internal/runtime/activity_service_test.go b/internal/runtime/activity_service_test.go
new file mode 100644
index 00000000..dc491a10
--- /dev/null
+++ b/internal/runtime/activity_service_test.go
@@ -0,0 +1,287 @@
+package runtime
+
+import (
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "go.uber.org/zap"
+)
+
+// TestEmitActivitySystemStart verifies system_start event emission (Spec 024)
+func TestEmitActivitySystemStart(t *testing.T) {
+ logger, err := zap.NewDevelopment()
+ require.NoError(t, err)
+ defer logger.Sync()
+
+ rt := &Runtime{
+ logger: logger,
+ eventSubs: make(map[chan Event]struct{}),
+ }
+
+ // Subscribe to events
+ eventChan := rt.SubscribeEvents()
+ defer rt.UnsubscribeEvents(eventChan)
+
+ done := make(chan Event, 1)
+
+ // Listen for activity.system.start event
+ go func() {
+ select {
+ case evt := <-eventChan:
+ if evt.Type == EventTypeActivitySystemStart {
+ done <- evt
+ }
+ case <-time.After(2 * time.Second):
+ t.Log("Timeout waiting for activity.system.start event")
+ }
+ }()
+
+ // Emit system start event
+ rt.EmitActivitySystemStart("v1.2.3", "127.0.0.1:8080", 150, "/path/to/config.json")
+
+ // Wait for event
+ select {
+ case evt := <-done:
+ assert.Equal(t, EventTypeActivitySystemStart, evt.Type, "Event type should be activity.system.start")
+ assert.NotNil(t, evt.Payload, "Event payload should not be nil")
+ assert.Equal(t, "v1.2.3", evt.Payload["version"], "Event should contain version")
+ assert.Equal(t, "127.0.0.1:8080", evt.Payload["listen_address"], "Event should contain listen_address")
+ assert.Equal(t, int64(150), evt.Payload["startup_duration_ms"], "Event should contain startup_duration_ms")
+ assert.Equal(t, "/path/to/config.json", evt.Payload["config_path"], "Event should contain config_path")
+ assert.NotZero(t, evt.Timestamp, "Event should have a timestamp")
+ case <-time.After(2 * time.Second):
+ t.Fatal("Did not receive activity.system.start event within timeout")
+ }
+}
+
+// TestEmitActivitySystemStop verifies system_stop event emission (Spec 024)
+func TestEmitActivitySystemStop(t *testing.T) {
+ logger, err := zap.NewDevelopment()
+ require.NoError(t, err)
+ defer logger.Sync()
+
+ rt := &Runtime{
+ logger: logger,
+ eventSubs: make(map[chan Event]struct{}),
+ }
+
+ // Subscribe to events
+ eventChan := rt.SubscribeEvents()
+ defer rt.UnsubscribeEvents(eventChan)
+
+ done := make(chan Event, 1)
+
+ // Listen for activity.system.stop event
+ go func() {
+ select {
+ case evt := <-eventChan:
+ if evt.Type == EventTypeActivitySystemStop {
+ done <- evt
+ }
+ case <-time.After(2 * time.Second):
+ t.Log("Timeout waiting for activity.system.stop event")
+ }
+ }()
+
+ // Emit system stop event
+ rt.EmitActivitySystemStop("signal", "SIGTERM", 3600, "")
+
+ // Wait for event
+ select {
+ case evt := <-done:
+ assert.Equal(t, EventTypeActivitySystemStop, evt.Type, "Event type should be activity.system.stop")
+ assert.NotNil(t, evt.Payload, "Event payload should not be nil")
+ assert.Equal(t, "signal", evt.Payload["reason"], "Event should contain reason")
+ assert.Equal(t, "SIGTERM", evt.Payload["signal"], "Event should contain signal")
+ assert.Equal(t, int64(3600), evt.Payload["uptime_seconds"], "Event should contain uptime_seconds")
+ assert.Equal(t, "", evt.Payload["error_message"], "Event should contain error_message")
+ assert.NotZero(t, evt.Timestamp, "Event should have a timestamp")
+ case <-time.After(2 * time.Second):
+ t.Fatal("Did not receive activity.system.stop event within timeout")
+ }
+}
+
+// TestEmitActivitySystemStop_WithError verifies system_stop event includes error info
+func TestEmitActivitySystemStop_WithError(t *testing.T) {
+ logger, err := zap.NewDevelopment()
+ require.NoError(t, err)
+ defer logger.Sync()
+
+ rt := &Runtime{
+ logger: logger,
+ eventSubs: make(map[chan Event]struct{}),
+ }
+
+ // Subscribe to events
+ eventChan := rt.SubscribeEvents()
+ defer rt.UnsubscribeEvents(eventChan)
+
+ done := make(chan Event, 1)
+
+ // Listen for activity.system.stop event
+ go func() {
+ select {
+ case evt := <-eventChan:
+ if evt.Type == EventTypeActivitySystemStop {
+ done <- evt
+ }
+ case <-time.After(2 * time.Second):
+ t.Log("Timeout waiting for activity.system.stop event")
+ }
+ }()
+
+ // Emit system stop event with error
+ rt.EmitActivitySystemStop("error", "", 120, "database connection lost")
+
+ // Wait for event
+ select {
+ case evt := <-done:
+ assert.Equal(t, EventTypeActivitySystemStop, evt.Type)
+ assert.Equal(t, "error", evt.Payload["reason"])
+ assert.Equal(t, "", evt.Payload["signal"])
+ assert.Equal(t, int64(120), evt.Payload["uptime_seconds"])
+ assert.Equal(t, "database connection lost", evt.Payload["error_message"])
+ case <-time.After(2 * time.Second):
+ t.Fatal("Did not receive activity.system.stop event within timeout")
+ }
+}
+
+// TestEmitActivityInternalToolCall verifies internal_tool_call event emission (Spec 024)
+func TestEmitActivityInternalToolCall(t *testing.T) {
+ logger, err := zap.NewDevelopment()
+ require.NoError(t, err)
+ defer logger.Sync()
+
+ rt := &Runtime{
+ logger: logger,
+ eventSubs: make(map[chan Event]struct{}),
+ }
+
+ // Subscribe to events
+ eventChan := rt.SubscribeEvents()
+ defer rt.UnsubscribeEvents(eventChan)
+
+ done := make(chan Event, 1)
+
+ // Listen for activity.internal_tool_call.completed event
+ go func() {
+ select {
+ case evt := <-eventChan:
+ if evt.Type == EventTypeActivityInternalToolCall {
+ done <- evt
+ }
+ case <-time.After(2 * time.Second):
+ t.Log("Timeout waiting for activity.internal_tool_call.completed event")
+ }
+ }()
+
+ // Emit internal tool call event
+ intent := map[string]interface{}{
+ "operation_type": "read",
+ "data_sensitivity": "public",
+ }
+ testArgs := map[string]interface{}{
+ "username": "octocat",
+ }
+ testResponse := map[string]interface{}{
+ "login": "octocat",
+ "id": 1,
+ }
+ rt.EmitActivityInternalToolCall(
+ "call_tool_read",
+ "github",
+ "get_user",
+ "call_tool_read",
+ "sess-123",
+ "req-456",
+ "success",
+ "",
+ 250,
+ testArgs,
+ testResponse,
+ intent,
+ )
+
+ // Wait for event
+ select {
+ case evt := <-done:
+ assert.Equal(t, EventTypeActivityInternalToolCall, evt.Type)
+ assert.Equal(t, "call_tool_read", evt.Payload["internal_tool_name"])
+ assert.Equal(t, "github", evt.Payload["target_server"])
+ assert.Equal(t, "get_user", evt.Payload["target_tool"])
+ assert.Equal(t, "call_tool_read", evt.Payload["tool_variant"])
+ assert.Equal(t, "sess-123", evt.Payload["session_id"])
+ assert.Equal(t, "req-456", evt.Payload["request_id"])
+ assert.Equal(t, "success", evt.Payload["status"])
+ assert.Equal(t, "", evt.Payload["error_message"])
+ assert.Equal(t, int64(250), evt.Payload["duration_ms"])
+ assert.NotNil(t, evt.Payload["intent"])
+ // Verify arguments and response are captured
+ assert.NotNil(t, evt.Payload["arguments"])
+ args := evt.Payload["arguments"].(map[string]interface{})
+ assert.Equal(t, "octocat", args["username"])
+ assert.NotNil(t, evt.Payload["response"])
+ resp := evt.Payload["response"].(map[string]interface{})
+ assert.Equal(t, "octocat", resp["login"])
+ case <-time.After(2 * time.Second):
+ t.Fatal("Did not receive activity.internal_tool_call.completed event within timeout")
+ }
+}
+
+// TestEmitActivityConfigChange verifies config_change event emission (Spec 024)
+func TestEmitActivityConfigChange(t *testing.T) {
+ logger, err := zap.NewDevelopment()
+ require.NoError(t, err)
+ defer logger.Sync()
+
+ rt := &Runtime{
+ logger: logger,
+ eventSubs: make(map[chan Event]struct{}),
+ }
+
+ // Subscribe to events
+ eventChan := rt.SubscribeEvents()
+ defer rt.UnsubscribeEvents(eventChan)
+
+ done := make(chan Event, 1)
+
+ // Listen for activity.config_change event
+ go func() {
+ select {
+ case evt := <-eventChan:
+ if evt.Type == EventTypeActivityConfigChange {
+ done <- evt
+ }
+ case <-time.After(2 * time.Second):
+ t.Log("Timeout waiting for activity.config_change event")
+ }
+ }()
+
+ // Emit config change event
+ prevValues := map[string]interface{}{"enabled": true}
+ newValues := map[string]interface{}{"enabled": false}
+ rt.EmitActivityConfigChange(
+ "server_updated",
+ "github",
+ "mcp",
+ []string{"enabled"},
+ prevValues,
+ newValues,
+ )
+
+ // Wait for event
+ select {
+ case evt := <-done:
+ assert.Equal(t, EventTypeActivityConfigChange, evt.Type)
+ assert.Equal(t, "server_updated", evt.Payload["action"])
+ assert.Equal(t, "github", evt.Payload["affected_entity"])
+ assert.Equal(t, "mcp", evt.Payload["source"])
+ assert.NotNil(t, evt.Payload["changed_fields"])
+ assert.NotNil(t, evt.Payload["previous_values"])
+ assert.NotNil(t, evt.Payload["new_values"])
+ case <-time.After(2 * time.Second):
+ t.Fatal("Did not receive activity.config_change event within timeout")
+ }
+}
diff --git a/internal/runtime/event_bus.go b/internal/runtime/event_bus.go
index f6764728..7db30913 100644
--- a/internal/runtime/event_bus.go
+++ b/internal/runtime/event_bus.go
@@ -102,9 +102,10 @@ func (r *Runtime) EmitActivityToolCallStarted(serverName, toolName, sessionID, r
// EmitActivityToolCallCompleted emits an event when a tool execution finishes.
// This is used to track activity for observability and debugging.
// source indicates how the call was triggered: "mcp", "cli", or "api"
+// arguments is the input parameters passed to the tool call
// toolVariant is the MCP tool variant used (call_tool_read/write/destructive) - optional
// intent is the intent declaration metadata - optional
-func (r *Runtime) EmitActivityToolCallCompleted(serverName, toolName, sessionID, requestID, source, status, errorMsg string, durationMs int64, response string, responseTruncated bool, toolVariant string, intent map[string]interface{}) {
+func (r *Runtime) EmitActivityToolCallCompleted(serverName, toolName, sessionID, requestID, source, status, errorMsg string, durationMs int64, arguments map[string]interface{}, response string, responseTruncated bool, toolVariant string, intent map[string]interface{}) {
payload := map[string]any{
"server_name": serverName,
"tool_name": toolName,
@@ -117,6 +118,10 @@ func (r *Runtime) EmitActivityToolCallCompleted(serverName, toolName, sessionID,
"response": response,
"response_truncated": responseTruncated,
}
+ // Add arguments if provided
+ if arguments != nil {
+ payload["arguments"] = arguments
+ }
// Add intent metadata if provided (Spec 018)
if toolVariant != "" {
payload["tool_variant"] = toolVariant
@@ -148,3 +153,81 @@ func (r *Runtime) EmitActivityQuarantineChange(serverName string, quarantined bo
}
r.publishEvent(newEvent(EventTypeActivityQuarantineChange, payload))
}
+
+// EmitActivitySystemStart emits an event when MCPProxy server starts (Spec 024).
+func (r *Runtime) EmitActivitySystemStart(version, listenAddress string, startupDurationMs int64, configPath string) {
+ payload := map[string]any{
+ "version": version,
+ "listen_address": listenAddress,
+ "startup_duration_ms": startupDurationMs,
+ "config_path": configPath,
+ }
+ r.publishEvent(newEvent(EventTypeActivitySystemStart, payload))
+}
+
+// EmitActivitySystemStop emits an event when MCPProxy server stops (Spec 024).
+func (r *Runtime) EmitActivitySystemStop(reason, signal string, uptimeSeconds int64, errorMsg string) {
+ payload := map[string]any{
+ "reason": reason,
+ "signal": signal,
+ "uptime_seconds": uptimeSeconds,
+ "error_message": errorMsg,
+ }
+ r.publishEvent(newEvent(EventTypeActivitySystemStop, payload))
+}
+
+// EmitActivityInternalToolCall emits an event when an internal tool is called (Spec 024).
+// internalToolName is the name of the internal tool (retrieve_tools, call_tool_read, etc.)
+// targetServer and targetTool are used for call_tool_* handlers
+// arguments contains the input parameters, response contains the output
+// intent is the intent declaration metadata
+func (r *Runtime) EmitActivityInternalToolCall(internalToolName, targetServer, targetTool, toolVariant, sessionID, requestID, status, errorMsg string, durationMs int64, arguments map[string]interface{}, response interface{}, intent map[string]interface{}) {
+ payload := map[string]any{
+ "internal_tool_name": internalToolName,
+ "session_id": sessionID,
+ "request_id": requestID,
+ "status": status,
+ "error_message": errorMsg,
+ "duration_ms": durationMs,
+ }
+ if targetServer != "" {
+ payload["target_server"] = targetServer
+ }
+ if targetTool != "" {
+ payload["target_tool"] = targetTool
+ }
+ if toolVariant != "" {
+ payload["tool_variant"] = toolVariant
+ }
+ if arguments != nil {
+ payload["arguments"] = arguments
+ }
+ if response != nil {
+ payload["response"] = response
+ }
+ if intent != nil {
+ payload["intent"] = intent
+ }
+ r.publishEvent(newEvent(EventTypeActivityInternalToolCall, payload))
+}
+
+// EmitActivityConfigChange emits an event when configuration changes (Spec 024).
+// action is one of: server_added, server_removed, server_updated, settings_changed
+// source indicates how the change was triggered: "mcp", "cli", or "api"
+func (r *Runtime) EmitActivityConfigChange(action, affectedEntity, source string, changedFields []string, previousValues, newValues map[string]interface{}) {
+ payload := map[string]any{
+ "action": action,
+ "affected_entity": affectedEntity,
+ "source": source,
+ }
+ if len(changedFields) > 0 {
+ payload["changed_fields"] = changedFields
+ }
+ if previousValues != nil {
+ payload["previous_values"] = previousValues
+ }
+ if newValues != nil {
+ payload["new_values"] = newValues
+ }
+ r.publishEvent(newEvent(EventTypeActivityConfigChange, payload))
+}
diff --git a/internal/runtime/events.go b/internal/runtime/events.go
index 68e3246e..4399eeed 100644
--- a/internal/runtime/events.go
+++ b/internal/runtime/events.go
@@ -28,6 +28,16 @@ const (
EventTypeActivityPolicyDecision EventType = "activity.policy_decision"
// EventTypeActivityQuarantineChange is emitted when a server's quarantine state changes.
EventTypeActivityQuarantineChange EventType = "activity.quarantine_change"
+
+ // Spec 024: Expanded Activity Log events
+ // EventTypeActivitySystemStart is emitted when MCPProxy server starts.
+ EventTypeActivitySystemStart EventType = "activity.system.start"
+ // EventTypeActivitySystemStop is emitted when MCPProxy server stops.
+ EventTypeActivitySystemStop EventType = "activity.system.stop"
+ // EventTypeActivityInternalToolCall is emitted when an internal tool (retrieve_tools, call_tool_*, etc.) completes.
+ EventTypeActivityInternalToolCall EventType = "activity.internal_tool_call.completed"
+ // EventTypeActivityConfigChange is emitted when configuration changes (server add/remove/update).
+ EventTypeActivityConfigChange EventType = "activity.config_change"
)
// Event is a typed notification published by the runtime event bus.
diff --git a/internal/runtime/lifecycle.go b/internal/runtime/lifecycle.go
index 590ddd12..1aea0bc0 100644
--- a/internal/runtime/lifecycle.go
+++ b/internal/runtime/lifecycle.go
@@ -895,6 +895,13 @@ func (r *Runtime) EnableServer(serverName string, enabled bool) error {
"enabled": enabled,
})
+ // Emit config change activity for audit trail (Spec 024)
+ action := "server_disabled"
+ if enabled {
+ action = "server_enabled"
+ }
+ r.EmitActivityConfigChange(action, serverName, "api", []string{"enabled"}, map[string]interface{}{"enabled": !enabled}, map[string]interface{}{"enabled": enabled})
+
r.HandleUpstreamServerChange(r.AppContext())
return nil
diff --git a/internal/server/e2e_test.go b/internal/server/e2e_test.go
index 1872754c..86f49706 100644
--- a/internal/server/e2e_test.go
+++ b/internal/server/e2e_test.go
@@ -1858,6 +1858,251 @@ func TestE2E_RequestID_ActivityFiltering(t *testing.T) {
t.Log("✅ All Request ID Activity Filtering E2E tests passed")
}
+// ============================================================================
+// Activity Log Filtering E2E Tests (Spec 024)
+// ============================================================================
+// These tests verify that activity log filtering works correctly,
+// including the exclusion of successful call_tool_* internal tool calls.
+
+// TestE2E_Activity_ExcludeCallToolSuccess verifies that successful call_tool_*
+// internal tool calls are excluded by default from activity listings.
+// Spec: 024-expand-activity-log
+func TestE2E_Activity_ExcludeCallToolSuccess(t *testing.T) {
+ env := NewTestEnvironment(t)
+ defer env.Cleanup()
+
+ baseURL := strings.TrimSuffix(env.proxyAddr, "/mcp")
+ apiKey := "test-api-key-e2e"
+
+ mcpClient := env.CreateProxyClient()
+ defer mcpClient.Close()
+ env.ConnectClient(mcpClient)
+
+ ctx := context.Background()
+
+ // Step 1: Make a tool call to generate both tool_call and internal_tool_call records
+ t.Run("Make tool call to generate activity", func(t *testing.T) {
+ callRequest := mcp.CallToolRequest{}
+ callRequest.Params.Name = "call_tool_read"
+ callRequest.Params.Arguments = map[string]interface{}{
+ "name": "test-server-fixture:echo_tool",
+ "args_json": `{"message": "test-activity-filtering"}`,
+ "intent": map[string]interface{}{
+ "operation_type": "read",
+ "reason": "E2E test for activity filtering",
+ },
+ }
+
+ result, err := mcpClient.CallTool(ctx, callRequest)
+ require.NoError(t, err)
+ // Tool call may fail if test-server-fixture doesn't exist, but that's OK
+ // The activity will still be logged
+ t.Logf("Tool call result (may fail, activities still logged): isError=%v", result.IsError)
+ })
+
+ // Give some time for activities to be persisted
+ time.Sleep(100 * time.Millisecond)
+
+ // Step 2: Query activities without include_call_tool (default: exclude successful call_tool_*)
+ t.Run("Default query excludes successful call_tool_*", func(t *testing.T) {
+ req, err := http.NewRequest("GET", baseURL+"/api/v1/activity?limit=50", nil)
+ require.NoError(t, err)
+ req.Header.Set("X-API-Key", apiKey)
+
+ resp, err := http.DefaultClient.Do(req)
+ require.NoError(t, err)
+ defer resp.Body.Close()
+
+ require.Equal(t, http.StatusOK, resp.StatusCode)
+
+ var activityResp struct {
+ Success bool `json:"success"`
+ Data struct {
+ Activities []struct {
+ ID string `json:"id"`
+ Type string `json:"type"`
+ ToolName string `json:"tool_name"`
+ Status string `json:"status"`
+ } `json:"activities"`
+ Total int `json:"total"`
+ } `json:"data"`
+ }
+ err = json.NewDecoder(resp.Body).Decode(&activityResp)
+ require.NoError(t, err)
+
+ // Check that no successful call_tool_* internal_tool_call records are in the response
+ for _, act := range activityResp.Data.Activities {
+ if act.Type == "internal_tool_call" && strings.HasPrefix(act.ToolName, "call_tool_") {
+ // If we find a call_tool_* internal_tool_call, it must be a failure
+ assert.NotEqual(t, "success", act.Status,
+ "Successful call_tool_* internal_tool_call should be excluded by default")
+ }
+ }
+ t.Logf("✅ Default query returned %d activities, no successful call_tool_* found", activityResp.Data.Total)
+ })
+
+ // Step 3: Query activities with include_call_tool=true (should include all)
+ t.Run("include_call_tool=true shows all internal tool calls", func(t *testing.T) {
+ req, err := http.NewRequest("GET", baseURL+"/api/v1/activity?limit=50&include_call_tool=true", nil)
+ require.NoError(t, err)
+ req.Header.Set("X-API-Key", apiKey)
+
+ resp, err := http.DefaultClient.Do(req)
+ require.NoError(t, err)
+ defer resp.Body.Close()
+
+ require.Equal(t, http.StatusOK, resp.StatusCode)
+
+ var activityResp struct {
+ Success bool `json:"success"`
+ Data struct {
+ Activities []struct {
+ ID string `json:"id"`
+ Type string `json:"type"`
+ ToolName string `json:"tool_name"`
+ Status string `json:"status"`
+ } `json:"activities"`
+ Total int `json:"total"`
+ } `json:"data"`
+ }
+ err = json.NewDecoder(resp.Body).Decode(&activityResp)
+ require.NoError(t, err)
+
+ // Just verify the parameter is accepted - may or may not have call_tool_* entries
+ t.Logf("✅ include_call_tool=true query returned %d activities", activityResp.Data.Total)
+ })
+
+ t.Log("✅ All Activity call_tool_* filtering E2E tests passed")
+}
+
+// TestE2E_Activity_MultiTypeFilter verifies that multi-type filtering works correctly.
+// Spec: 024-expand-activity-log
+func TestE2E_Activity_MultiTypeFilter(t *testing.T) {
+ env := NewTestEnvironment(t)
+ defer env.Cleanup()
+
+ baseURL := strings.TrimSuffix(env.proxyAddr, "/mcp")
+ apiKey := "test-api-key-e2e"
+
+ // Step 1: Query with single type filter
+ t.Run("Single type filter", func(t *testing.T) {
+ req, err := http.NewRequest("GET", baseURL+"/api/v1/activity?type=system_start", nil)
+ require.NoError(t, err)
+ req.Header.Set("X-API-Key", apiKey)
+
+ resp, err := http.DefaultClient.Do(req)
+ require.NoError(t, err)
+ defer resp.Body.Close()
+
+ require.Equal(t, http.StatusOK, resp.StatusCode)
+
+ var activityResp struct {
+ Success bool `json:"success"`
+ Data struct {
+ Activities []struct {
+ ID string `json:"id"`
+ Type string `json:"type"`
+ } `json:"activities"`
+ Total int `json:"total"`
+ } `json:"data"`
+ }
+ err = json.NewDecoder(resp.Body).Decode(&activityResp)
+ require.NoError(t, err)
+
+ // All returned activities should be system_start type
+ for _, act := range activityResp.Data.Activities {
+ assert.Equal(t, "system_start", act.Type, "Filtered activities should only be system_start type")
+ }
+ t.Logf("✅ Single type filter returned %d system_start activities", activityResp.Data.Total)
+ })
+
+ // Step 2: Query with multi-type filter (comma-separated)
+ t.Run("Multi-type filter with comma separation", func(t *testing.T) {
+ req, err := http.NewRequest("GET", baseURL+"/api/v1/activity?type=system_start,system_stop,config_change", nil)
+ require.NoError(t, err)
+ req.Header.Set("X-API-Key", apiKey)
+
+ resp, err := http.DefaultClient.Do(req)
+ require.NoError(t, err)
+ defer resp.Body.Close()
+
+ require.Equal(t, http.StatusOK, resp.StatusCode)
+
+ var activityResp struct {
+ Success bool `json:"success"`
+ Data struct {
+ Activities []struct {
+ ID string `json:"id"`
+ Type string `json:"type"`
+ } `json:"activities"`
+ Total int `json:"total"`
+ } `json:"data"`
+ }
+ err = json.NewDecoder(resp.Body).Decode(&activityResp)
+ require.NoError(t, err)
+
+ // All returned activities should be one of the filtered types
+ validTypes := map[string]bool{
+ "system_start": true,
+ "system_stop": true,
+ "config_change": true,
+ }
+ for _, act := range activityResp.Data.Activities {
+ assert.True(t, validTypes[act.Type], "Activity type %s should be in filter list", act.Type)
+ }
+ t.Logf("✅ Multi-type filter returned %d activities of types system_start/system_stop/config_change", activityResp.Data.Total)
+ })
+
+ t.Log("✅ All Activity multi-type filtering E2E tests passed")
+}
+
+// TestE2E_Activity_Summary verifies that activity summary endpoint works correctly.
+// Spec: 024-expand-activity-log
+func TestE2E_Activity_Summary(t *testing.T) {
+ env := NewTestEnvironment(t)
+ defer env.Cleanup()
+
+ baseURL := strings.TrimSuffix(env.proxyAddr, "/mcp")
+ apiKey := "test-api-key-e2e"
+
+ t.Run("Get activity summary for 24h period", func(t *testing.T) {
+ req, err := http.NewRequest("GET", baseURL+"/api/v1/activity/summary?period=24h", nil)
+ require.NoError(t, err)
+ req.Header.Set("X-API-Key", apiKey)
+
+ resp, err := http.DefaultClient.Do(req)
+ require.NoError(t, err)
+ defer resp.Body.Close()
+
+ require.Equal(t, http.StatusOK, resp.StatusCode)
+
+ var summaryResp struct {
+ Success bool `json:"success"`
+ Data struct {
+ Period string `json:"period"`
+ TotalCount int `json:"total_count"`
+ SuccessCount int `json:"success_count"`
+ ErrorCount int `json:"error_count"`
+ BlockedCount int `json:"blocked_count"`
+ } `json:"data"`
+ }
+ err = json.NewDecoder(resp.Body).Decode(&summaryResp)
+ require.NoError(t, err)
+
+ assert.True(t, summaryResp.Success, "Summary request should succeed")
+ assert.Equal(t, "24h", summaryResp.Data.Period, "Period should be 24h")
+ // Total should be sum of success + error + blocked
+ assert.GreaterOrEqual(t, summaryResp.Data.TotalCount,
+ summaryResp.Data.SuccessCount+summaryResp.Data.ErrorCount+summaryResp.Data.BlockedCount,
+ "Total should be >= sum of status counts")
+ t.Logf("✅ Activity summary: total=%d, success=%d, error=%d, blocked=%d",
+ summaryResp.Data.TotalCount, summaryResp.Data.SuccessCount,
+ summaryResp.Data.ErrorCount, summaryResp.Data.BlockedCount)
+ })
+
+ t.Log("✅ All Activity summary E2E tests passed")
+}
+
// ============================================================================
// Smart Config Patching E2E Tests (Spec 023, Issues #239, #240)
// ============================================================================
diff --git a/internal/server/mcp.go b/internal/server/mcp.go
index 1fbf469a..72949786 100644
--- a/internal/server/mcp.go
+++ b/internal/server/mcp.go
@@ -248,11 +248,12 @@ func (p *MCPProxyServer) emitActivityToolCallStarted(serverName, toolName, sessi
// emitActivityToolCallCompleted safely emits a tool call completion event if runtime is available
// source indicates how the call was triggered: "mcp", "cli", or "api"
+// arguments is the input parameters passed to the tool call
// toolVariant is the MCP tool variant used (call_tool_read/write/destructive) - optional
// intent is the intent declaration metadata - optional
-func (p *MCPProxyServer) emitActivityToolCallCompleted(serverName, toolName, sessionID, requestID, source, status, errorMsg string, durationMs int64, response string, responseTruncated bool, toolVariant string, intent map[string]interface{}) {
+func (p *MCPProxyServer) emitActivityToolCallCompleted(serverName, toolName, sessionID, requestID, source, status, errorMsg string, durationMs int64, arguments map[string]interface{}, response string, responseTruncated bool, toolVariant string, intent map[string]interface{}) {
if p.mainServer != nil && p.mainServer.runtime != nil {
- p.mainServer.runtime.EmitActivityToolCallCompleted(serverName, toolName, sessionID, requestID, source, status, errorMsg, durationMs, response, responseTruncated, toolVariant, intent)
+ p.mainServer.runtime.EmitActivityToolCallCompleted(serverName, toolName, sessionID, requestID, source, status, errorMsg, durationMs, arguments, response, responseTruncated, toolVariant, intent)
}
}
@@ -262,6 +263,17 @@ func (p *MCPProxyServer) emitActivityPolicyDecision(serverName, toolName, sessio
}
}
+// emitActivityInternalToolCall safely emits an internal tool call completion event (Spec 024)
+// internalToolName is the name of the internal tool (retrieve_tools, call_tool_read, etc.)
+// targetServer and targetTool are used for call_tool_* handlers
+// arguments contains the input parameters, response contains the output
+// intent is the intent declaration metadata
+func (p *MCPProxyServer) emitActivityInternalToolCall(internalToolName, targetServer, targetTool, toolVariant, sessionID, requestID, status, errorMsg string, durationMs int64, arguments map[string]interface{}, response interface{}, intent map[string]interface{}) {
+ if p.mainServer != nil && p.mainServer.runtime != nil {
+ p.mainServer.runtime.EmitActivityInternalToolCall(internalToolName, targetServer, targetTool, toolVariant, sessionID, requestID, status, errorMsg, durationMs, arguments, response, intent)
+ }
+}
+
// registerTools registers all proxy tools with the MCP server
func (p *MCPProxyServer) registerTools(_ bool) {
// retrieve_tools - THE PRIMARY TOOL FOR DISCOVERING TOOLS - Enhanced with clear instructions
@@ -606,8 +618,18 @@ Common issues:
// handleSearchServers implements the search_servers functionality
func (p *MCPProxyServer) handleSearchServers(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ startTime := time.Now()
+
+ // Extract session info for activity logging (Spec 024)
+ var sessionID string
+ if sess := mcpserver.ClientSessionFromContext(ctx); sess != nil {
+ sessionID = sess.SessionID()
+ }
+ requestID := fmt.Sprintf("%d-search_servers", time.Now().UnixNano())
+
registry, err := request.RequireString("registry")
if err != nil {
+ p.emitActivityInternalToolCall("search_servers", "", "", "", sessionID, requestID, "error", err.Error(), time.Since(startTime).Milliseconds(), nil, nil, nil)
return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'registry': %v", err)), nil
}
@@ -616,6 +638,18 @@ func (p *MCPProxyServer) handleSearchServers(ctx context.Context, request mcp.Ca
tag := request.GetString("tag", "")
limit := int(request.GetFloat("limit", 10.0)) // Default limit of 10
+ // Build arguments map for activity logging (Spec 024)
+ args := map[string]interface{}{
+ "registry": registry,
+ "limit": limit,
+ }
+ if search != "" {
+ args["search"] = search
+ }
+ if tag != "" {
+ args["tag"] = tag
+ }
+
// Create experiments guesser if repository checking is enabled
var guesser *experiments.Guesser
if p.config != nil && p.config.CheckServerRepo {
@@ -630,6 +664,7 @@ func (p *MCPProxyServer) handleSearchServers(ctx context.Context, request mcp.Ca
zap.String("search", search),
zap.String("tag", tag),
zap.Error(err))
+ p.emitActivityInternalToolCall("search_servers", "", "", "", sessionID, requestID, "error", err.Error(), time.Since(startTime).Milliseconds(), args, nil, nil)
return mcp.NewToolResultError(fmt.Sprintf("Search failed: %v", err)), nil
}
@@ -653,14 +688,27 @@ func (p *MCPProxyServer) handleSearchServers(ctx context.Context, request mcp.Ca
jsonResult, err := json.Marshal(response)
if err != nil {
+ p.emitActivityInternalToolCall("search_servers", "", "", "", sessionID, requestID, "error", err.Error(), time.Since(startTime).Milliseconds(), args, nil, nil)
return mcp.NewToolResultError(fmt.Sprintf("Failed to serialize results: %v", err)), nil
}
+ // Spec 024: Emit success event with args and response
+ p.emitActivityInternalToolCall("search_servers", "", "", "", sessionID, requestID, "success", "", time.Since(startTime).Milliseconds(), args, response, nil)
+
return mcp.NewToolResultText(string(jsonResult)), nil
}
// handleListRegistries implements the list_registries functionality
-func (p *MCPProxyServer) handleListRegistries(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+func (p *MCPProxyServer) handleListRegistries(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ startTime := time.Now()
+
+ // Extract session info for activity logging (Spec 024)
+ var sessionID string
+ if sess := mcpserver.ClientSessionFromContext(ctx); sess != nil {
+ sessionID = sess.SessionID()
+ }
+ requestID := fmt.Sprintf("%d-list_registries", time.Now().UnixNano())
+
registriesList := []map[string]interface{}{}
allRegistries := registries.ListRegistries()
for i := range allRegistries {
@@ -675,22 +723,39 @@ func (p *MCPProxyServer) handleListRegistries(_ context.Context, _ mcp.CallToolR
})
}
- jsonResult, err := json.Marshal(map[string]interface{}{
+ response := map[string]interface{}{
"registries": registriesList,
"total": len(registriesList),
"message": "Available MCP registries. Use 'search_servers' tool with a registry ID to find servers.",
- })
+ }
+
+ jsonResult, err := json.Marshal(response)
if err != nil {
+ p.emitActivityInternalToolCall("list_registries", "", "", "", sessionID, requestID, "error", err.Error(), time.Since(startTime).Milliseconds(), nil, nil, nil)
return mcp.NewToolResultError(fmt.Sprintf("Failed to serialize registries: %v", err)), nil
}
+ // Spec 024: Emit success event with response
+ p.emitActivityInternalToolCall("list_registries", "", "", "", sessionID, requestID, "success", "", time.Since(startTime).Milliseconds(), nil, response, nil)
+
return mcp.NewToolResultText(string(jsonResult)), nil
}
// handleRetrieveTools implements the retrieve_tools functionality
-func (p *MCPProxyServer) handleRetrieveTools(_ context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+func (p *MCPProxyServer) handleRetrieveTools(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ startTime := time.Now()
+
+ // Extract session info for activity logging (Spec 024)
+ var sessionID string
+ if sess := mcpserver.ClientSessionFromContext(ctx); sess != nil {
+ sessionID = sess.SessionID()
+ }
+ requestID := fmt.Sprintf("%d-retrieve_tools", time.Now().UnixNano())
+
query, err := request.RequireString("query")
if err != nil {
+ // Emit internal tool call event for error case
+ p.emitActivityInternalToolCall("retrieve_tools", "", "", "", sessionID, requestID, "error", err.Error(), time.Since(startTime).Milliseconds(), nil, nil, nil)
return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'query': %v", err)), nil
}
@@ -700,6 +765,21 @@ func (p *MCPProxyServer) handleRetrieveTools(_ context.Context, request mcp.Call
debugMode := request.GetBool("debug", false)
explainTool := request.GetString("explain_tool", "")
+ // Build arguments map for activity logging (Spec 024)
+ args := map[string]interface{}{
+ "query": query,
+ "limit": limit,
+ }
+ if includeStats {
+ args["include_stats"] = true
+ }
+ if debugMode {
+ args["debug"] = true
+ }
+ if explainTool != "" {
+ args["explain_tool"] = explainTool
+ }
+
// Validate limit
if limit > 100 {
limit = 100
@@ -709,6 +789,7 @@ func (p *MCPProxyServer) handleRetrieveTools(_ context.Context, request mcp.Call
results, err := p.index.Search(query, limit)
if err != nil {
p.logger.Error("Search failed", zap.String("query", query), zap.Error(err))
+ p.emitActivityInternalToolCall("retrieve_tools", "", "", "", sessionID, requestID, "error", err.Error(), time.Since(startTime).Milliseconds(), args, nil, nil)
return mcp.NewToolResultError(fmt.Sprintf("Search failed: %v", err)), nil
}
@@ -809,9 +890,13 @@ func (p *MCPProxyServer) handleRetrieveTools(_ context.Context, request mcp.Call
jsonResult, err := json.Marshal(response)
if err != nil {
+ p.emitActivityInternalToolCall("retrieve_tools", "", "", "", sessionID, requestID, "error", err.Error(), time.Since(startTime).Milliseconds(), args, nil, nil)
return mcp.NewToolResultError(fmt.Sprintf("Failed to serialize results: %v", err)), nil
}
+ // Emit success event with args and response (Spec 024)
+ p.emitActivityInternalToolCall("retrieve_tools", "", "", "", sessionID, requestID, "success", "", time.Since(startTime).Milliseconds(), args, response, nil)
+
return mcp.NewToolResultText(string(jsonResult)), nil
}
@@ -832,6 +917,9 @@ func (p *MCPProxyServer) handleCallToolDestructive(ctx context.Context, request
// handleCallToolVariant is the common handler for all call_tool_* variants (Spec 018)
func (p *MCPProxyServer) handleCallToolVariant(ctx context.Context, request mcp.CallToolRequest, toolVariant string) (*mcp.CallToolResult, error) {
+ // Spec 024: Track start time and context for internal tool call logging
+ internalStartTime := time.Now()
+
// Add panic recovery to ensure server resilience
defer func() {
if r := recover(); r != nil {
@@ -949,6 +1037,32 @@ func (p *MCPProxyServer) handleCallToolVariant(ctx context.Context, request mcp.
return errResult, nil
}
+ // Extract session information from context early (needed for activity events, including early failures)
+ var sessionID, clientName, clientVersion string
+ if sess := mcpserver.ClientSessionFromContext(ctx); sess != nil {
+ sessionID = sess.SessionID()
+ if sessInfo := p.sessionStore.GetSession(sessionID); sessInfo != nil {
+ clientName = sessInfo.ClientName
+ clientVersion = sessInfo.ClientVersion
+ }
+ }
+
+ // Determine activity source from context (CLI/API calls set this, MCP calls have session)
+ activitySource := "mcp"
+ if reqSource := reqcontext.GetRequestSource(ctx); reqSource != reqcontext.SourceUnknown {
+ switch reqSource {
+ case reqcontext.SourceCLI:
+ activitySource = "cli"
+ case reqcontext.SourceRESTAPI:
+ activitySource = "api"
+ default:
+ activitySource = "mcp"
+ }
+ }
+
+ // Generate requestID for activity tracking
+ requestID := fmt.Sprintf("%d-%s-%s", time.Now().UnixNano(), serverName, actualToolName)
+
// Check if server is quarantined before calling tool
serverConfig, err := p.storage.GetUpstreamServer(serverName)
if err == nil && serverConfig.Quarantined {
@@ -966,43 +1080,35 @@ func (p *MCPProxyServer) handleCallToolVariant(ctx context.Context, request mcp.
if client, exists := p.upstreamManager.GetClient(serverName); exists {
if !client.IsConnected() {
state := client.GetState()
+ errMsg := ""
if client.IsConnecting() {
- return mcp.NewToolResultError(fmt.Sprintf("Server '%s' is currently connecting - please wait for connection to complete (state: %s)", serverName, state.String())), nil
+ errMsg = fmt.Sprintf("Server '%s' is currently connecting - please wait for connection to complete (state: %s)", serverName, state.String())
+ } else {
+ errMsg = fmt.Sprintf("Server '%s' is not connected (state: %s) - use 'upstream_servers' tool to check server configuration", serverName, state.String())
+ }
+ // Log the early failure to activity (Spec 024)
+ var intentMap map[string]interface{}
+ if intent != nil {
+ intentMap = intent.ToMap()
}
- return mcp.NewToolResultError(fmt.Sprintf("Server '%s' is not connected (state: %s) - use 'upstream_servers' tool to check server configuration", serverName, state.String())), nil
+ p.emitActivityToolCallStarted(serverName, actualToolName, sessionID, requestID, activitySource, args)
+ p.emitActivityToolCallCompleted(serverName, actualToolName, sessionID, requestID, activitySource, "error", errMsg, 0, args, errMsg, false, toolVariant, intentMap)
+ return mcp.NewToolResultError(errMsg), nil
}
} else {
p.logger.Error("handleCallToolVariant: no client found for server",
zap.String("server_name", serverName))
- return mcp.NewToolResultError(fmt.Sprintf("No client found for server: %s", serverName)), nil
- }
-
- // Extract session information from context (needed for activity events)
- var sessionID, clientName, clientVersion string
- if sess := mcpserver.ClientSessionFromContext(ctx); sess != nil {
- sessionID = sess.SessionID()
- if sessInfo := p.sessionStore.GetSession(sessionID); sessInfo != nil {
- clientName = sessInfo.ClientName
- clientVersion = sessInfo.ClientVersion
- }
- }
-
- // Determine activity source from context (CLI/API calls set this, MCP calls have session)
- activitySource := "mcp"
- if reqSource := reqcontext.GetRequestSource(ctx); reqSource != reqcontext.SourceUnknown {
- switch reqSource {
- case reqcontext.SourceCLI:
- activitySource = "cli"
- case reqcontext.SourceRESTAPI:
- activitySource = "api"
- default:
- activitySource = "mcp"
+ errMsg := fmt.Sprintf("No client found for server: %s", serverName)
+ // Log the early failure to activity (Spec 024)
+ var intentMap map[string]interface{}
+ if intent != nil {
+ intentMap = intent.ToMap()
}
+ p.emitActivityToolCallStarted(serverName, actualToolName, sessionID, requestID, activitySource, args)
+ p.emitActivityToolCallCompleted(serverName, actualToolName, sessionID, requestID, activitySource, "error", errMsg, 0, args, errMsg, false, toolVariant, intentMap)
+ return mcp.NewToolResultError(errMsg), nil
}
- // Generate requestID for activity tracking
- requestID := fmt.Sprintf("%d-%s-%s", time.Now().UnixNano(), serverName, actualToolName)
-
// Emit activity started event with determined source
p.emitActivityToolCallStarted(serverName, actualToolName, sessionID, requestID, activitySource, args)
@@ -1090,7 +1196,11 @@ func (p *MCPProxyServer) handleCallToolVariant(ctx context.Context, request mcp.
if intent != nil {
intentMap = intent.ToMap()
}
- p.emitActivityToolCallCompleted(serverName, actualToolName, sessionID, requestID, activitySource, "error", err.Error(), duration.Milliseconds(), "", false, toolVariant, intentMap)
+ p.emitActivityToolCallCompleted(serverName, actualToolName, sessionID, requestID, activitySource, "error", err.Error(), duration.Milliseconds(), args, "", false, toolVariant, intentMap)
+
+ // Spec 024: Emit internal tool call event for error
+ internalToolName := "call_tool_" + intent.OperationType // e.g., "call_tool_read"
+ p.emitActivityInternalToolCall(internalToolName, serverName, actualToolName, toolVariant, sessionID, requestID, "error", err.Error(), time.Since(internalStartTime).Milliseconds(), args, nil, intentMap)
return p.createDetailedErrorResponse(err, serverName, actualToolName), nil
}
@@ -1190,7 +1300,11 @@ func (p *MCPProxyServer) handleCallToolVariant(ctx context.Context, request mcp.
if intent != nil {
intentMap = intent.ToMap()
}
- p.emitActivityToolCallCompleted(serverName, actualToolName, sessionID, requestID, activitySource, "success", "", duration.Milliseconds(), response, responseTruncated, toolVariant, intentMap)
+ p.emitActivityToolCallCompleted(serverName, actualToolName, sessionID, requestID, activitySource, "success", "", duration.Milliseconds(), args, response, responseTruncated, toolVariant, intentMap)
+
+ // Spec 024: Emit internal tool call event for success
+ internalToolName := "call_tool_" + intent.OperationType // e.g., "call_tool_read"
+ p.emitActivityInternalToolCall(internalToolName, serverName, actualToolName, toolVariant, sessionID, requestID, "success", "", time.Since(internalStartTime).Milliseconds(), args, result, intentMap)
return mcp.NewToolResultText(response), nil
}
@@ -1294,20 +1408,40 @@ func (p *MCPProxyServer) handleCallTool(ctx context.Context, request mcp.CallToo
zap.String("actual_tool_name", actualToolName),
zap.Any("args", args))
+ // Extract session information from context early (needed for activity events, including early failures)
+ var sessionID, clientName, clientVersion string
+ if sess := mcpserver.ClientSessionFromContext(ctx); sess != nil {
+ sessionID = sess.SessionID()
+ if sessInfo := p.sessionStore.GetSession(sessionID); sessInfo != nil {
+ clientName = sessInfo.ClientName
+ clientVersion = sessInfo.ClientVersion
+ }
+ }
+
+ // Determine activity source from context (CLI/API calls set this, MCP calls have session)
+ activitySource := "mcp"
+ if reqSource := reqcontext.GetRequestSource(ctx); reqSource != reqcontext.SourceUnknown {
+ switch reqSource {
+ case reqcontext.SourceCLI:
+ activitySource = "cli"
+ case reqcontext.SourceRESTAPI:
+ activitySource = "api"
+ default:
+ activitySource = "mcp"
+ }
+ }
+
+ // Generate requestID for activity tracking
+ requestID := fmt.Sprintf("%d-%s-%s", time.Now().UnixNano(), serverName, actualToolName)
+
// Check if server is quarantined before calling tool
serverConfig, err := p.storage.GetUpstreamServer(serverName)
if err == nil && serverConfig.Quarantined {
p.logger.Debug("handleCallTool: server is quarantined",
zap.String("server_name", serverName))
- // Extract session ID for activity logging
- var quarantineSessionID string
- if sess := mcpserver.ClientSessionFromContext(ctx); sess != nil {
- quarantineSessionID = sess.SessionID()
- }
-
// Emit policy decision event for quarantine block
- p.emitActivityPolicyDecision(serverName, actualToolName, quarantineSessionID, "blocked", "Server is quarantined for security review")
+ p.emitActivityPolicyDecision(serverName, actualToolName, sessionID, "blocked", "Server is quarantined for security review")
// Server is in quarantine - return security warning with tool analysis
return p.handleQuarantinedToolCall(ctx, serverName, actualToolName, args), nil
@@ -1325,47 +1459,31 @@ func (p *MCPProxyServer) handleCallTool(ctx context.Context, request mcp.CallToo
if !client.IsConnected() {
state := client.GetState()
+ errMsg := ""
if client.IsConnecting() {
- return mcp.NewToolResultError(fmt.Sprintf("Server '%s' is currently connecting - please wait for connection to complete (state: %s)", serverName, state.String())), nil
+ errMsg = fmt.Sprintf("Server '%s' is currently connecting - please wait for connection to complete (state: %s)", serverName, state.String())
+ } else {
+ errMsg = fmt.Sprintf("Server '%s' is not connected (state: %s) - use 'upstream_servers' tool to check server configuration", serverName, state.String())
}
- return mcp.NewToolResultError(fmt.Sprintf("Server '%s' is not connected (state: %s) - use 'upstream_servers' tool to check server configuration", serverName, state.String())), nil
+ // Log the early failure to activity (Spec 024)
+ p.emitActivityToolCallStarted(serverName, actualToolName, sessionID, requestID, activitySource, args)
+ p.emitActivityToolCallCompleted(serverName, actualToolName, sessionID, requestID, activitySource, "error", errMsg, 0, args, errMsg, false, "", nil)
+ return mcp.NewToolResultError(errMsg), nil
}
} else {
p.logger.Error("handleCallTool: no client found for server",
zap.String("server_name", serverName))
- return mcp.NewToolResultError(fmt.Sprintf("No client found for server: %s", serverName)), nil
+ errMsg := fmt.Sprintf("No client found for server: %s", serverName)
+ // Log the early failure to activity (Spec 024)
+ p.emitActivityToolCallStarted(serverName, actualToolName, sessionID, requestID, activitySource, args)
+ p.emitActivityToolCallCompleted(serverName, actualToolName, sessionID, requestID, activitySource, "error", errMsg, 0, args, errMsg, false, "", nil)
+ return mcp.NewToolResultError(errMsg), nil
}
p.logger.Debug("handleCallTool: calling upstream manager",
zap.String("tool_name", toolName),
zap.String("server_name", serverName))
- // Extract session information from context (needed for activity events)
- var sessionID, clientName, clientVersion string
- if sess := mcpserver.ClientSessionFromContext(ctx); sess != nil {
- sessionID = sess.SessionID()
- if sessInfo := p.sessionStore.GetSession(sessionID); sessInfo != nil {
- clientName = sessInfo.ClientName
- clientVersion = sessInfo.ClientVersion
- }
- }
-
- // Determine activity source from context (CLI/API calls set this, MCP calls have session)
- activitySource := "mcp"
- if reqSource := reqcontext.GetRequestSource(ctx); reqSource != reqcontext.SourceUnknown {
- switch reqSource {
- case reqcontext.SourceCLI:
- activitySource = "cli"
- case reqcontext.SourceRESTAPI:
- activitySource = "api"
- default:
- activitySource = "mcp"
- }
- }
-
- // Generate requestID for activity tracking
- requestID := fmt.Sprintf("%d-%s-%s", time.Now().UnixNano(), serverName, actualToolName)
-
// Emit activity started event with determined source
p.emitActivityToolCallStarted(serverName, actualToolName, sessionID, requestID, activitySource, args)
@@ -1470,7 +1588,7 @@ func (p *MCPProxyServer) handleCallTool(ctx context.Context, request mcp.CallToo
}
// Emit activity completed event for error with determined source (legacy - no intent)
- p.emitActivityToolCallCompleted(serverName, actualToolName, sessionID, requestID, activitySource, "error", err.Error(), duration.Milliseconds(), "", false, "", nil)
+ p.emitActivityToolCallCompleted(serverName, actualToolName, sessionID, requestID, activitySource, "error", err.Error(), duration.Milliseconds(), args, "", false, "", nil)
return p.createDetailedErrorResponse(err, serverName, actualToolName), nil
}
@@ -1566,7 +1684,7 @@ func (p *MCPProxyServer) handleCallTool(ctx context.Context, request mcp.CallToo
// Emit activity completed event for success with determined source (legacy - no intent)
responseTruncated := tokenMetrics != nil && tokenMetrics.WasTruncated
- p.emitActivityToolCallCompleted(serverName, actualToolName, sessionID, requestID, activitySource, "success", "", duration.Milliseconds(), response, responseTruncated, "", nil)
+ p.emitActivityToolCallCompleted(serverName, actualToolName, sessionID, requestID, activitySource, "success", "", duration.Milliseconds(), args, response, responseTruncated, "", nil)
return mcp.NewToolResultText(response), nil
}
@@ -1626,19 +1744,39 @@ func (p *MCPProxyServer) handleQuarantinedToolCall(ctx context.Context, serverNa
// handleUpstreamServers implements upstream server management
func (p *MCPProxyServer) handleUpstreamServers(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ startTime := time.Now()
+
+ // Extract session info for activity logging (Spec 024)
+ var sessionID string
+ if sess := mcpserver.ClientSessionFromContext(ctx); sess != nil {
+ sessionID = sess.SessionID()
+ }
+ requestID := fmt.Sprintf("%d-upstream_servers", time.Now().UnixNano())
+
operation, err := request.RequireString("operation")
if err != nil {
+ p.emitActivityInternalToolCall("upstream_servers", "", "", "", sessionID, requestID, "error", err.Error(), time.Since(startTime).Milliseconds(), nil, nil, nil)
return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'operation': %v", err)), nil
}
+ // Build arguments map for activity logging (Spec 024)
+ args := map[string]interface{}{
+ "operation": operation,
+ }
+ if name := request.GetString("name", ""); name != "" {
+ args["name"] = name
+ }
+
// Security checks
if p.config.ReadOnlyMode {
if operation != operationList {
+ p.emitActivityInternalToolCall("upstream_servers", "", "", "", sessionID, requestID, "error", "Operation not allowed in read-only mode", time.Since(startTime).Milliseconds(), args, nil, nil)
return mcp.NewToolResultError("Operation not allowed in read-only mode"), nil
}
}
if p.config.DisableManagement {
+ p.emitActivityInternalToolCall("upstream_servers", "", "", "", sessionID, requestID, "error", "Server management is disabled for security", time.Since(startTime).Milliseconds(), args, nil, nil)
return mcp.NewToolResultError("Server management is disabled for security"), nil
}
@@ -1646,64 +1784,144 @@ func (p *MCPProxyServer) handleUpstreamServers(ctx context.Context, request mcp.
switch operation {
case operationAdd:
if !p.config.AllowServerAdd {
+ p.emitActivityInternalToolCall("upstream_servers", "", "", "", sessionID, requestID, "error", "Adding servers is not allowed", time.Since(startTime).Milliseconds(), args, nil, nil)
return mcp.NewToolResultError("Adding servers is not allowed"), nil
}
case operationRemove:
if !p.config.AllowServerRemove {
+ p.emitActivityInternalToolCall("upstream_servers", "", "", "", sessionID, requestID, "error", "Removing servers is not allowed", time.Since(startTime).Milliseconds(), args, nil, nil)
return mcp.NewToolResultError("Removing servers is not allowed"), nil
}
}
+ // Execute operation and track result
+ var result *mcp.CallToolResult
+ var opErr error
+
switch operation {
case operationList:
- return p.handleListUpstreams(ctx)
+ result, opErr = p.handleListUpstreams(ctx)
case operationAdd:
- return p.handleAddUpstream(ctx, request)
+ result, opErr = p.handleAddUpstream(ctx, request)
case operationRemove:
- return p.handleRemoveUpstream(ctx, request)
+ result, opErr = p.handleRemoveUpstream(ctx, request)
case "update":
- return p.handleUpdateUpstream(ctx, request)
+ result, opErr = p.handleUpdateUpstream(ctx, request)
case "patch":
- return p.handlePatchUpstream(ctx, request)
+ result, opErr = p.handlePatchUpstream(ctx, request)
case "tail_log":
- return p.handleTailLog(ctx, request)
+ result, opErr = p.handleTailLog(ctx, request)
case "enable":
- return p.handleEnableUpstream(ctx, request, true)
+ result, opErr = p.handleEnableUpstream(ctx, request, true)
case "disable":
- return p.handleEnableUpstream(ctx, request, false)
+ result, opErr = p.handleEnableUpstream(ctx, request, false)
case "restart":
- return p.handleRestartUpstream(ctx, request)
+ result, opErr = p.handleRestartUpstream(ctx, request)
default:
+ p.emitActivityInternalToolCall("upstream_servers", "", "", "", sessionID, requestID, "error", fmt.Sprintf("Unknown operation: %s", operation), time.Since(startTime).Milliseconds(), args, nil, nil)
return mcp.NewToolResultError(fmt.Sprintf("Unknown operation: %s", operation)), nil
}
+
+ // Extract response text for activity logging (Spec 024)
+ var responseText string
+ if result != nil && len(result.Content) > 0 {
+ if textContent, ok := result.Content[0].(mcp.TextContent); ok {
+ responseText = textContent.Text
+ }
+ }
+
+ // Spec 024: Emit activity event based on result with args and response
+ if opErr != nil {
+ p.emitActivityInternalToolCall("upstream_servers", "", "", "", sessionID, requestID, "error", opErr.Error(), time.Since(startTime).Milliseconds(), args, nil, nil)
+ } else if result != nil && result.IsError {
+ // Extract error message from result if available
+ errMsg := "operation failed"
+ if responseText != "" {
+ errMsg = responseText
+ }
+ p.emitActivityInternalToolCall("upstream_servers", "", "", "", sessionID, requestID, "error", errMsg, time.Since(startTime).Milliseconds(), args, nil, nil)
+ } else {
+ p.emitActivityInternalToolCall("upstream_servers", "", "", "", sessionID, requestID, "success", "", time.Since(startTime).Milliseconds(), args, responseText, nil)
+ }
+
+ return result, opErr
}
// handleQuarantineSecurity implements the quarantine_security functionality
func (p *MCPProxyServer) handleQuarantineSecurity(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ startTime := time.Now()
+
+ // Extract session info for activity logging (Spec 024)
+ var sessionID string
+ if sess := mcpserver.ClientSessionFromContext(ctx); sess != nil {
+ sessionID = sess.SessionID()
+ }
+ requestID := fmt.Sprintf("%d-quarantine_security", time.Now().UnixNano())
+
operation, err := request.RequireString("operation")
if err != nil {
+ p.emitActivityInternalToolCall("quarantine_security", "", "", "", sessionID, requestID, "error", err.Error(), time.Since(startTime).Milliseconds(), nil, nil, nil)
return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'operation': %v", err)), nil
}
+ // Build arguments map for activity logging (Spec 024)
+ args := map[string]interface{}{
+ "operation": operation,
+ }
+ if name := request.GetString("name", ""); name != "" {
+ args["name"] = name
+ }
+
// Security checks
if p.config.ReadOnlyMode {
+ p.emitActivityInternalToolCall("quarantine_security", "", "", "", sessionID, requestID, "error", "Quarantine operations not allowed in read-only mode", time.Since(startTime).Milliseconds(), args, nil, nil)
return mcp.NewToolResultError("Quarantine operations not allowed in read-only mode"), nil
}
if p.config.DisableManagement {
+ p.emitActivityInternalToolCall("quarantine_security", "", "", "", sessionID, requestID, "error", "Server management is disabled for security", time.Since(startTime).Milliseconds(), args, nil, nil)
return mcp.NewToolResultError("Server management is disabled for security"), nil
}
+ // Execute operation and track result
+ var result *mcp.CallToolResult
+ var opErr error
+
switch operation {
case "list_quarantined":
- return p.handleListQuarantinedUpstreams(ctx)
+ result, opErr = p.handleListQuarantinedUpstreams(ctx)
case "inspect_quarantined":
- return p.handleInspectQuarantinedTools(ctx, request)
+ result, opErr = p.handleInspectQuarantinedTools(ctx, request)
case "quarantine":
- return p.handleQuarantineUpstream(ctx, request)
+ result, opErr = p.handleQuarantineUpstream(ctx, request)
default:
+ p.emitActivityInternalToolCall("quarantine_security", "", "", "", sessionID, requestID, "error", fmt.Sprintf("Unknown quarantine operation: %s", operation), time.Since(startTime).Milliseconds(), args, nil, nil)
return mcp.NewToolResultError(fmt.Sprintf("Unknown quarantine operation: %s", operation)), nil
}
+
+ // Extract response text for activity logging (Spec 024)
+ var responseText string
+ if result != nil && len(result.Content) > 0 {
+ if textContent, ok := result.Content[0].(mcp.TextContent); ok {
+ responseText = textContent.Text
+ }
+ }
+
+ // Spec 024: Emit activity event based on result with args and response
+ if opErr != nil {
+ p.emitActivityInternalToolCall("quarantine_security", "", "", "", sessionID, requestID, "error", opErr.Error(), time.Since(startTime).Milliseconds(), args, nil, nil)
+ } else if result != nil && result.IsError {
+ // Extract error message from result if available
+ errMsg := "operation failed"
+ if responseText != "" {
+ errMsg = responseText
+ }
+ p.emitActivityInternalToolCall("quarantine_security", "", "", "", sessionID, requestID, "error", errMsg, time.Since(startTime).Milliseconds(), args, nil, nil)
+ } else {
+ p.emitActivityInternalToolCall("quarantine_security", "", "", "", sessionID, requestID, "success", "", time.Since(startTime).Milliseconds(), args, responseText, nil)
+ }
+
+ return result, opErr
}
func (p *MCPProxyServer) handleListUpstreams(_ context.Context) (*mcp.CallToolResult, error) {
@@ -2547,6 +2765,20 @@ func (p *MCPProxyServer) handleAddUpstream(ctx context.Context, request mcp.Call
// Continue anyway - UpdateConfig above already notified the supervisor
}
p.mainServer.OnUpstreamServerChange()
+
+ // Spec 024: Emit config change activity for server addition
+ newValues := map[string]interface{}{
+ "protocol": protocol,
+ "enabled": enabled,
+ "quarantined": quarantined,
+ }
+ if url != "" {
+ newValues["url"] = url
+ }
+ if command != "" {
+ newValues["command"] = command
+ }
+ p.mainServer.runtime.EmitActivityConfigChange("server_added", name, "mcp", nil, nil, newValues)
}
// Wait briefly for supervisor to reconcile and connect (if enabled)
@@ -2657,6 +2889,9 @@ func (p *MCPProxyServer) handleRemoveUpstream(_ context.Context, request mcp.Cal
p.logger.Error("Failed to save configuration after removing server", zap.Error(err))
}
p.mainServer.OnUpstreamServerChange()
+
+ // Spec 024: Emit config change activity for server removal
+ p.mainServer.runtime.EmitActivityConfigChange("server_removed", name, "mcp", nil, nil, nil)
}
jsonResult, err := json.Marshal(map[string]interface{}{
@@ -3097,9 +3332,19 @@ func (p *MCPProxyServer) explainToolRanking(query, targetTool string, results []
}
// handleReadCache implements the read_cache functionality
-func (p *MCPProxyServer) handleReadCache(_ context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+func (p *MCPProxyServer) handleReadCache(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ startTime := time.Now()
+
+ // Extract session info for activity logging (Spec 024)
+ var sessionID string
+ if sess := mcpserver.ClientSessionFromContext(ctx); sess != nil {
+ sessionID = sess.SessionID()
+ }
+ requestID := fmt.Sprintf("%d-read_cache", time.Now().UnixNano())
+
key, err := request.RequireString("key")
if err != nil {
+ p.emitActivityInternalToolCall("read_cache", "", "", "", sessionID, requestID, "error", err.Error(), time.Since(startTime).Milliseconds(), nil, nil, nil)
return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'key': %v", err)), nil
}
@@ -3107,26 +3352,40 @@ func (p *MCPProxyServer) handleReadCache(_ context.Context, request mcp.CallTool
offset := int(request.GetFloat("offset", 0))
limit := int(request.GetFloat("limit", 50))
+ // Build arguments map for activity logging (Spec 024)
+ args := map[string]interface{}{
+ "key": key,
+ "offset": offset,
+ "limit": limit,
+ }
+
// Validate parameters
if offset < 0 {
+ p.emitActivityInternalToolCall("read_cache", "", "", "", sessionID, requestID, "error", "Offset must be non-negative", time.Since(startTime).Milliseconds(), args, nil, nil)
return mcp.NewToolResultError("Offset must be non-negative"), nil
}
if limit <= 0 || limit > 1000 {
+ p.emitActivityInternalToolCall("read_cache", "", "", "", sessionID, requestID, "error", "Limit must be between 1 and 1000", time.Since(startTime).Milliseconds(), args, nil, nil)
return mcp.NewToolResultError("Limit must be between 1 and 1000"), nil
}
// Retrieve cached data
response, err := p.cacheManager.GetRecords(key, offset, limit)
if err != nil {
+ p.emitActivityInternalToolCall("read_cache", "", "", "", sessionID, requestID, "error", err.Error(), time.Since(startTime).Milliseconds(), args, nil, nil)
return mcp.NewToolResultError(fmt.Sprintf("Failed to retrieve cached data: %v", err)), nil
}
// Serialize response
jsonResult, err := json.Marshal(response)
if err != nil {
+ p.emitActivityInternalToolCall("read_cache", "", "", "", sessionID, requestID, "error", err.Error(), time.Since(startTime).Milliseconds(), args, nil, nil)
return mcp.NewToolResultError(fmt.Sprintf("Failed to serialize response: %v", err)), nil
}
+ // Spec 024: Emit success event with args and response
+ p.emitActivityInternalToolCall("read_cache", "", "", "", sessionID, requestID, "success", "", time.Since(startTime).Milliseconds(), args, response, nil)
+
return mcp.NewToolResultText(string(jsonResult)), nil
}
diff --git a/internal/server/mcp_code_execution.go b/internal/server/mcp_code_execution.go
index 52d69eb7..b9bd11dc 100644
--- a/internal/server/mcp_code_execution.go
+++ b/internal/server/mcp_code_execution.go
@@ -296,6 +296,22 @@ func (p *MCPProxyServer) handleCodeExecution(ctx context.Context, request mcp.Ca
return nil, fmt.Errorf("failed to serialize result: %w", err)
}
+ // Spec 024: Emit internal tool call event for code_execution
+ var status, errorMsg string
+ if result.Ok {
+ status = "success"
+ } else {
+ status = "error"
+ if result.Error != nil {
+ errorMsg = result.Error.Message
+ }
+ }
+ codeExecArgs := map[string]interface{}{
+ "code": code,
+ "input": options.Input,
+ }
+ p.emitActivityInternalToolCall("code_execution", "", "", "", sessionID, parentCallID, status, errorMsg, executionDuration.Milliseconds(), codeExecArgs, result, nil)
+
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.NewTextContent(string(resultJSON)),
diff --git a/internal/server/server.go b/internal/server/server.go
index 55a49dae..11fd9bfe 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -61,6 +61,13 @@ type Server struct {
statusCh chan interface{}
eventsCh chan runtime.Event
+
+ // Spec 024: Track server start time for lifecycle events
+ startTime time.Time
+
+ // Spec 024: Shutdown info for lifecycle events
+ shutdownReason string
+ shutdownSignal string
}
// NewServer creates a new server instance
@@ -221,6 +228,11 @@ func (s *Server) forwardRuntimeStatus() {
// Start starts the MCP proxy server
func (s *Server) Start(ctx context.Context) error {
+ // Spec 024: Track server start time for lifecycle events
+ s.mu.Lock()
+ s.startTime = time.Now()
+ s.mu.Unlock()
+
s.logger.Info("Starting MCP proxy server")
// Handle graceful shutdown when context is cancelled (for full application shutdown only)
@@ -286,6 +298,19 @@ func (s *Server) Start(ctx context.Context) error {
s.updateStatus(runtime.PhaseRunning, "Server is running in stdio mode")
s.runtime.SetRunning(true)
+ // Spec 024: Emit system_start activity event for stdio mode
+ startupDurationMs := time.Since(s.startTime).Milliseconds()
+ configPath := ""
+ if s.runtime != nil {
+ configPath = s.runtime.ConfigPath()
+ }
+ s.runtime.EmitActivitySystemStart(
+ httpapi.GetBuildVersion(),
+ "stdio",
+ startupDurationMs,
+ configPath,
+ )
+
// Serve using stdio (standard MCP transport)
if err := server.ServeStdio(s.mcpProxy.GetMCPServer()); err != nil {
return fmt.Errorf("MCP server error: %w", err)
@@ -296,6 +321,16 @@ func (s *Server) Start(ctx context.Context) error {
}
// discoverAndIndexTools discovers tools from upstream servers and indexes them
+
+// SetShutdownInfo sets the reason and signal for shutdown (Spec 024).
+// Call this before Shutdown() to include shutdown context in activity logs.
+func (s *Server) SetShutdownInfo(reason, signal string) {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ s.shutdownReason = reason
+ s.shutdownSignal = signal
+}
+
// Shutdown gracefully shuts down the server
func (s *Server) Shutdown() error {
s.mu.Lock()
@@ -306,8 +341,20 @@ func (s *Server) Shutdown() error {
}
s.shutdown = true
httpServer := s.httpServer
+ startTime := s.startTime
+ reason := s.shutdownReason
+ signal := s.shutdownSignal
s.mu.Unlock()
+ // Spec 024: Emit system_stop event before actual shutdown begins
+ if s.runtime != nil && !startTime.IsZero() {
+ uptimeSeconds := int64(time.Since(startTime).Seconds())
+ if reason == "" {
+ reason = "shutdown"
+ }
+ s.runtime.EmitActivitySystemStop(reason, signal, uptimeSeconds, "")
+ }
+
if s.eventsCh != nil {
s.runtime.UnsubscribeEvents(s.eventsCh)
}
@@ -1360,6 +1407,19 @@ func (s *Server) startCustomHTTPServer(ctx context.Context, streamableServer *se
// Broadcast running status with resolved listen address so readiness checks succeed immediately.
s.updateStatus(runtime.PhaseRunning, fmt.Sprintf("Server is running on %s", displayAddr))
+ // Spec 024: Emit system_start activity event
+ startupDurationMs := time.Since(s.startTime).Milliseconds()
+ configPath := ""
+ if s.runtime != nil {
+ configPath = s.runtime.ConfigPath()
+ }
+ s.runtime.EmitActivitySystemStart(
+ httpapi.GetBuildVersion(),
+ displayAddr,
+ startupDurationMs,
+ configPath,
+ )
+
// List all registered endpoints for visibility
allEndpoints := []string{
"/mcp", "/mcp/", // MCP protocol endpoints
diff --git a/internal/storage/activity_models.go b/internal/storage/activity_models.go
index 323a3153..4064babb 100644
--- a/internal/storage/activity_models.go
+++ b/internal/storage/activity_models.go
@@ -2,6 +2,7 @@ package storage
import (
"encoding/json"
+ "strings"
"time"
)
@@ -20,8 +21,28 @@ const (
ActivityTypeQuarantineChange ActivityType = "quarantine_change"
// ActivityTypeServerChange represents a server configuration change
ActivityTypeServerChange ActivityType = "server_change"
+ // ActivityTypeSystemStart represents MCPProxy server startup (Spec 024)
+ ActivityTypeSystemStart ActivityType = "system_start"
+ // ActivityTypeSystemStop represents MCPProxy server shutdown (Spec 024)
+ ActivityTypeSystemStop ActivityType = "system_stop"
+ // ActivityTypeInternalToolCall represents internal MCP tool calls like retrieve_tools, call_tool_* (Spec 024)
+ ActivityTypeInternalToolCall ActivityType = "internal_tool_call"
+ // ActivityTypeConfigChange represents configuration changes like server add/remove/update (Spec 024)
+ ActivityTypeConfigChange ActivityType = "config_change"
)
+// ValidActivityTypes is the list of all valid activity types for filtering (Spec 024)
+var ValidActivityTypes = []string{
+ string(ActivityTypeToolCall),
+ string(ActivityTypePolicyDecision),
+ string(ActivityTypeQuarantineChange),
+ string(ActivityTypeServerChange),
+ string(ActivityTypeSystemStart),
+ string(ActivityTypeSystemStop),
+ string(ActivityTypeInternalToolCall),
+ string(ActivityTypeConfigChange),
+}
+
// ActivitySource indicates how the activity was triggered
type ActivitySource string
@@ -65,7 +86,7 @@ func (a *ActivityRecord) UnmarshalBinary(data []byte) error {
// ActivityFilter represents query parameters for filtering activity records
type ActivityFilter struct {
- Type string // Filter by activity type
+ Types []string // Filter by activity types (Spec 024: supports multiple types with OR logic)
Server string // Filter by server name
Tool string // Filter by tool name
SessionID string // Filter by MCP session
@@ -76,13 +97,20 @@ type ActivityFilter struct {
Offset int // Pagination offset
IntentType string // Filter by intent operation type: read, write, destructive (Spec 018)
RequestID string // Filter by HTTP request ID for correlation (Spec 021)
+
+ // ExcludeCallToolSuccess filters out successful call_tool_* internal tool calls.
+ // These appear as duplicates since the actual upstream tool call is also logged.
+ // Failed call_tool_* calls are still shown (no corresponding tool_call entry).
+ // Default: true (to avoid duplicate entries in UI/CLI)
+ ExcludeCallToolSuccess bool
}
// DefaultActivityFilter returns an ActivityFilter with sensible defaults
func DefaultActivityFilter() ActivityFilter {
return ActivityFilter{
- Limit: 50,
- Offset: 0,
+ Limit: 50,
+ Offset: 0,
+ ExcludeCallToolSuccess: true, // Exclude successful call_tool_* to avoid duplicates
}
}
@@ -101,9 +129,18 @@ func (f *ActivityFilter) Validate() {
// Matches checks if an activity record matches the filter criteria
func (f *ActivityFilter) Matches(record *ActivityRecord) bool {
- // Check type filter
- if f.Type != "" && string(record.Type) != f.Type {
- return false
+ // Check types filter (Spec 024: OR logic for multiple types)
+ if len(f.Types) > 0 {
+ typeMatches := false
+ for _, t := range f.Types {
+ if string(record.Type) == t {
+ typeMatches = true
+ break
+ }
+ }
+ if !typeMatches {
+ return false
+ }
}
// Check server filter
@@ -147,6 +184,17 @@ func (f *ActivityFilter) Matches(record *ActivityRecord) bool {
return false
}
+ // Exclude 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 f.ExcludeCallToolSuccess {
+ if record.Type == ActivityTypeInternalToolCall &&
+ record.Status == "success" &&
+ strings.HasPrefix(record.ToolName, "call_tool_") {
+ return false
+ }
+ }
+
return true
}
diff --git a/internal/storage/activity_test.go b/internal/storage/activity_test.go
index e87ae1a0..7840821c 100644
--- a/internal/storage/activity_test.go
+++ b/internal/storage/activity_test.go
@@ -140,13 +140,28 @@ func TestActivityFilter_Matches(t *testing.T) {
matches: true,
},
{
- name: "type matches",
- filter: ActivityFilter{Type: "tool_call"},
+ name: "single type matches",
+ filter: ActivityFilter{Types: []string{"tool_call"}},
matches: true,
},
{
- name: "type does not match",
- filter: ActivityFilter{Type: "policy_decision"},
+ name: "single type does not match",
+ filter: ActivityFilter{Types: []string{"policy_decision"}},
+ matches: false,
+ },
+ {
+ name: "multiple types OR logic - first matches",
+ filter: ActivityFilter{Types: []string{"tool_call", "policy_decision"}},
+ matches: true,
+ },
+ {
+ name: "multiple types OR logic - second matches",
+ filter: ActivityFilter{Types: []string{"policy_decision", "tool_call"}},
+ matches: true,
+ },
+ {
+ name: "multiple types OR logic - none match",
+ filter: ActivityFilter{Types: []string{"policy_decision", "quarantine_change"}},
matches: false,
},
{
@@ -204,7 +219,7 @@ func TestActivityFilter_Matches(t *testing.T) {
{
name: "multiple filters all match",
filter: ActivityFilter{
- Type: "tool_call",
+ Types: []string{"tool_call"},
Server: "github",
Status: "success",
},
@@ -213,7 +228,7 @@ func TestActivityFilter_Matches(t *testing.T) {
{
name: "multiple filters one fails",
filter: ActivityFilter{
- Type: "tool_call",
+ Types: []string{"tool_call"},
Server: "gitlab", // does not match
Status: "success",
},
@@ -401,8 +416,8 @@ func TestListActivities_Filtering(t *testing.T) {
require.NoError(t, err)
}
- // Filter by type
- filter := ActivityFilter{Type: "tool_call", Limit: 50}
+ // Filter by single type
+ filter := ActivityFilter{Types: []string{"tool_call"}, Limit: 50}
records, total, err := manager.ListActivities(filter)
require.NoError(t, err)
assert.Equal(t, 3, total)
@@ -420,11 +435,17 @@ func TestListActivities_Filtering(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, 2, total)
- // Combined filters
- filter = ActivityFilter{Type: "tool_call", Server: "github", Limit: 50}
+ // Combined filters with single type
+ filter = ActivityFilter{Types: []string{"tool_call"}, Server: "github", Limit: 50}
_, total, err = manager.ListActivities(filter)
require.NoError(t, err)
assert.Equal(t, 2, total)
+
+ // Multi-type filter (Spec 024)
+ filter = ActivityFilter{Types: []string{"tool_call", "policy_decision"}, Limit: 50}
+ _, total, err = manager.ListActivities(filter)
+ require.NoError(t, err)
+ assert.Equal(t, 4, total) // 3 tool_call + 1 policy_decision
}
func TestDeleteActivity(t *testing.T) {
diff --git a/oas/docs.go b/oas/docs.go
index be7ce401..3397f603 100644
--- a/oas/docs.go
+++ b/oas/docs.go
@@ -9,7 +9,7 @@ const docTemplate = `{
"components": {"schemas":{"config.Config":{"properties":{"activity_cleanup_interval_min":{"description":"Background cleanup interval in minutes (default: 60)","type":"integer"},"activity_max_records":{"description":"Max records before pruning (default: 100000)","type":"integer"},"activity_max_response_size":{"description":"Response truncation limit in bytes (default: 65536)","type":"integer"},"activity_retention_days":{"description":"Activity logging settings (RFC-003)","type":"integer"},"allow_server_add":{"type":"boolean"},"allow_server_remove":{"type":"boolean"},"api_key":{"description":"Security settings","type":"string"},"call_tool_timeout":{"type":"string"},"check_server_repo":{"description":"Repository detection settings","type":"boolean"},"code_execution_max_tool_calls":{"description":"Max tool calls per execution (0 = unlimited, default: 0)","type":"integer"},"code_execution_pool_size":{"description":"JavaScript runtime pool size (default: 10)","type":"integer"},"code_execution_timeout_ms":{"description":"Timeout in milliseconds (default: 120000, max: 600000)","type":"integer"},"data_dir":{"type":"string"},"debug_search":{"type":"boolean"},"disable_management":{"type":"boolean"},"docker_isolation":{"$ref":"#/components/schemas/config.DockerIsolationConfig"},"docker_recovery":{"$ref":"#/components/schemas/config.DockerRecoveryConfig"},"enable_code_execution":{"description":"Code execution settings","type":"boolean"},"enable_prompts":{"description":"Prompts settings","type":"boolean"},"enable_socket":{"description":"Enable Unix socket/named pipe for local IPC (default: true)","type":"boolean"},"enable_tray":{"type":"boolean"},"environment":{"$ref":"#/components/schemas/secureenv.EnvConfig"},"features":{"$ref":"#/components/schemas/config.FeatureFlags"},"intent_declaration":{"$ref":"#/components/schemas/config.IntentDeclarationConfig"},"listen":{"type":"string"},"logging":{"$ref":"#/components/schemas/config.LogConfig"},"mcpServers":{"items":{"$ref":"#/components/schemas/config.ServerConfig"},"type":"array","uniqueItems":false},"oauth_expiry_warning_hours":{"description":"Health status settings","type":"number"},"read_only_mode":{"type":"boolean"},"registries":{"description":"Registries configuration for MCP server discovery","items":{"$ref":"#/components/schemas/config.RegistryEntry"},"type":"array","uniqueItems":false},"tls":{"$ref":"#/components/schemas/config.TLSConfig"},"tokenizer":{"$ref":"#/components/schemas/config.TokenizerConfig"},"tool_response_limit":{"type":"integer"},"tools_limit":{"type":"integer"},"top_k":{"type":"integer"},"tray_endpoint":{"description":"Tray endpoint override (unix:// or npipe://)","type":"string"}},"type":"object"},"config.DockerIsolationConfig":{"description":"Docker isolation settings","properties":{"cpu_limit":{"description":"CPU limit for containers","type":"string"},"default_images":{"additionalProperties":{"type":"string"},"description":"Map of runtime type to Docker image","type":"object"},"enabled":{"description":"Global enable/disable for Docker isolation","type":"boolean"},"extra_args":{"description":"Additional docker run arguments","items":{"type":"string"},"type":"array","uniqueItems":false},"log_driver":{"description":"Docker log driver (default: json-file)","type":"string"},"log_max_files":{"description":"Maximum number of log files (default: 3)","type":"string"},"log_max_size":{"description":"Maximum size of log files (default: 100m)","type":"string"},"memory_limit":{"description":"Memory limit for containers","type":"string"},"network_mode":{"description":"Docker network mode (default: bridge)","type":"string"},"registry":{"description":"Custom registry (defaults to docker.io)","type":"string"},"timeout":{"description":"Container startup timeout","type":"string"}},"type":"object"},"config.DockerRecoveryConfig":{"description":"Docker recovery settings","properties":{"enabled":{"description":"Enable Docker recovery monitoring (default: true)","type":"boolean"},"max_retries":{"description":"Maximum retry attempts (0 = unlimited)","type":"integer"},"notify_on_failure":{"description":"Show notification on recovery failure (default: true)","type":"boolean"},"notify_on_retry":{"description":"Show notification on each retry (default: false)","type":"boolean"},"notify_on_start":{"description":"Show notification when recovery starts (default: true)","type":"boolean"},"notify_on_success":{"description":"Show notification on successful recovery (default: true)","type":"boolean"},"persistent_state":{"description":"Save recovery state across restarts (default: true)","type":"boolean"}},"type":"object"},"config.FeatureFlags":{"description":"Feature flags for modular functionality","properties":{"enable_async_storage":{"type":"boolean"},"enable_caching":{"type":"boolean"},"enable_contract_tests":{"type":"boolean"},"enable_debug_logging":{"description":"Development features","type":"boolean"},"enable_docker_isolation":{"type":"boolean"},"enable_event_bus":{"type":"boolean"},"enable_health_checks":{"type":"boolean"},"enable_metrics":{"type":"boolean"},"enable_oauth":{"description":"Security features","type":"boolean"},"enable_observability":{"description":"Observability features","type":"boolean"},"enable_quarantine":{"type":"boolean"},"enable_runtime":{"description":"Runtime features","type":"boolean"},"enable_search":{"description":"Storage features","type":"boolean"},"enable_sse":{"type":"boolean"},"enable_tracing":{"type":"boolean"},"enable_tray":{"type":"boolean"},"enable_web_ui":{"description":"UI features","type":"boolean"}},"type":"object"},"config.IntentDeclarationConfig":{"description":"Intent declaration settings (Spec 018)","properties":{"strict_server_validation":{"description":"StrictServerValidation controls whether server annotation mismatches\ncause rejection (true) or just warnings (false).\nDefault: true (reject mismatches)","type":"boolean"}},"type":"object"},"config.IsolationConfig":{"description":"Per-server isolation settings","properties":{"enabled":{"description":"Enable Docker isolation for this server (nil = inherit global)","type":"boolean"},"extra_args":{"description":"Additional docker run arguments for this server","items":{"type":"string"},"type":"array","uniqueItems":false},"image":{"description":"Custom Docker image (overrides default)","type":"string"},"log_driver":{"description":"Docker log driver override for this server","type":"string"},"log_max_files":{"description":"Maximum number of log files override","type":"string"},"log_max_size":{"description":"Maximum size of log files override","type":"string"},"network_mode":{"description":"Custom network mode for this server","type":"string"},"working_dir":{"description":"Custom working directory in container","type":"string"}},"type":"object"},"config.LogConfig":{"description":"Logging configuration","properties":{"compress":{"type":"boolean"},"enable_console":{"type":"boolean"},"enable_file":{"type":"boolean"},"filename":{"type":"string"},"json_format":{"type":"boolean"},"level":{"type":"string"},"log_dir":{"description":"Custom log directory","type":"string"},"max_age":{"description":"days","type":"integer"},"max_backups":{"description":"number of backup files","type":"integer"},"max_size":{"description":"MB","type":"integer"}},"type":"object"},"config.OAuthConfig":{"description":"OAuth configuration (keep even when empty to signal OAuth requirement)","properties":{"client_id":{"type":"string"},"client_secret":{"type":"string"},"extra_params":{"additionalProperties":{"type":"string"},"description":"Additional OAuth parameters (e.g., RFC 8707 resource)","type":"object"},"pkce_enabled":{"type":"boolean"},"redirect_uri":{"type":"string"},"scopes":{"items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"config.RegistryEntry":{"properties":{"count":{"description":"number or string","type":"string"},"description":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"protocol":{"type":"string"},"servers_url":{"type":"string"},"tags":{"items":{"type":"string"},"type":"array","uniqueItems":false},"url":{"type":"string"}},"type":"object"},"config.ServerConfig":{"properties":{"args":{"items":{"type":"string"},"type":"array","uniqueItems":false},"command":{"type":"string"},"created":{"type":"string"},"enabled":{"type":"boolean"},"env":{"additionalProperties":{"type":"string"},"type":"object"},"headers":{"additionalProperties":{"type":"string"},"description":"For HTTP servers","type":"object"},"isolation":{"$ref":"#/components/schemas/config.IsolationConfig"},"name":{"type":"string"},"oauth":{"$ref":"#/components/schemas/config.OAuthConfig"},"protocol":{"description":"stdio, http, sse, streamable-http, auto","type":"string"},"quarantined":{"description":"Security quarantine status","type":"boolean"},"updated":{"type":"string"},"url":{"type":"string"},"working_dir":{"description":"Working directory for stdio servers","type":"string"}},"type":"object"},"config.TLSConfig":{"description":"TLS configuration","properties":{"certs_dir":{"description":"Directory for certificates","type":"string"},"enabled":{"description":"Enable HTTPS","type":"boolean"},"hsts":{"description":"Enable HTTP Strict Transport Security","type":"boolean"},"require_client_cert":{"description":"Enable mTLS","type":"boolean"}},"type":"object"},"config.TokenizerConfig":{"description":"Tokenizer configuration for token counting","properties":{"default_model":{"description":"Default model for tokenization (e.g., \"gpt-4\")","type":"string"},"enabled":{"description":"Enable token counting","type":"boolean"},"encoding":{"description":"Default encoding (e.g., \"cl100k_base\")","type":"string"}},"type":"object"},"contracts.APIResponse":{"allOf":[{"$ref":"#/components/schemas/data"}],"properties":{"data":{"type":"object"},"error":{"type":"string"},"request_id":{"type":"string"},"success":{"type":"boolean"}},"type":"object"},"contracts.ActivityDetailResponse":{"properties":{"activity":{"$ref":"#/components/schemas/contracts.ActivityRecord"}},"type":"object"},"contracts.ActivityListResponse":{"properties":{"activities":{"items":{"$ref":"#/components/schemas/contracts.ActivityRecord"},"type":"array","uniqueItems":false},"limit":{"type":"integer"},"offset":{"type":"integer"},"total":{"type":"integer"}},"type":"object"},"contracts.ActivityRecord":{"properties":{"arguments":{"description":"Tool call arguments","type":"object"},"duration_ms":{"description":"Execution duration in milliseconds","type":"integer"},"error_message":{"description":"Error details if status is \"error\"","type":"string"},"id":{"description":"Unique identifier (ULID format)","type":"string"},"metadata":{"description":"Additional context-specific data","type":"object"},"request_id":{"description":"HTTP request ID for correlation","type":"string"},"response":{"description":"Tool response (potentially truncated)","type":"string"},"response_truncated":{"description":"True if response was truncated","type":"boolean"},"server_name":{"description":"Name of upstream MCP server","type":"string"},"session_id":{"description":"MCP session ID for correlation","type":"string"},"source":{"$ref":"#/components/schemas/contracts.ActivitySource"},"status":{"description":"Result status: \"success\", \"error\", \"blocked\"","type":"string"},"timestamp":{"description":"When activity occurred","type":"string"},"tool_name":{"description":"Name of tool called","type":"string"},"type":{"$ref":"#/components/schemas/contracts.ActivityType"}},"type":"object"},"contracts.ActivitySource":{"description":"How activity was triggered: \"mcp\", \"cli\", \"api\"","type":"string","x-enum-varnames":["ActivitySourceMCP","ActivitySourceCLI","ActivitySourceAPI"]},"contracts.ActivitySummaryResponse":{"properties":{"blocked_count":{"description":"Count of blocked activities","type":"integer"},"end_time":{"description":"End of the period (RFC3339)","type":"string"},"error_count":{"description":"Count of error activities","type":"integer"},"period":{"description":"Time period (1h, 24h, 7d, 30d)","type":"string"},"start_time":{"description":"Start of the period (RFC3339)","type":"string"},"success_count":{"description":"Count of successful activities","type":"integer"},"top_servers":{"description":"Top servers by activity count","items":{"$ref":"#/components/schemas/contracts.ActivityTopServer"},"type":"array","uniqueItems":false},"top_tools":{"description":"Top tools by activity count","items":{"$ref":"#/components/schemas/contracts.ActivityTopTool"},"type":"array","uniqueItems":false},"total_count":{"description":"Total activity count","type":"integer"}},"type":"object"},"contracts.ActivityTopServer":{"properties":{"count":{"description":"Activity count","type":"integer"},"name":{"description":"Server name","type":"string"}},"type":"object"},"contracts.ActivityTopTool":{"properties":{"count":{"description":"Activity count","type":"integer"},"server":{"description":"Server name","type":"string"},"tool":{"description":"Tool name","type":"string"}},"type":"object"},"contracts.ActivityType":{"description":"Type of activity","type":"string","x-enum-varnames":["ActivityTypeToolCall","ActivityTypePolicyDecision","ActivityTypeQuarantineChange","ActivityTypeServerChange"]},"contracts.ConfigApplyResult":{"properties":{"applied_immediately":{"type":"boolean"},"changed_fields":{"items":{"type":"string"},"type":"array","uniqueItems":false},"requires_restart":{"type":"boolean"},"restart_reason":{"type":"string"},"success":{"type":"boolean"},"validation_errors":{"items":{"$ref":"#/components/schemas/contracts.ValidationError"},"type":"array","uniqueItems":false}},"type":"object"},"contracts.DCRStatus":{"properties":{"attempted":{"type":"boolean"},"error":{"type":"string"},"status_code":{"type":"integer"},"success":{"type":"boolean"}},"type":"object"},"contracts.Diagnostics":{"properties":{"docker_status":{"$ref":"#/components/schemas/contracts.DockerStatus"},"missing_secrets":{"description":"Renamed to avoid conflict","items":{"$ref":"#/components/schemas/contracts.MissingSecretInfo"},"type":"array","uniqueItems":false},"oauth_issues":{"description":"OAuth parameter mismatches","items":{"$ref":"#/components/schemas/contracts.OAuthIssue"},"type":"array","uniqueItems":false},"oauth_required":{"items":{"$ref":"#/components/schemas/contracts.OAuthRequirement"},"type":"array","uniqueItems":false},"runtime_warnings":{"items":{"type":"string"},"type":"array","uniqueItems":false},"timestamp":{"type":"string"},"total_issues":{"type":"integer"},"upstream_errors":{"items":{"$ref":"#/components/schemas/contracts.UpstreamError"},"type":"array","uniqueItems":false}},"type":"object"},"contracts.DockerStatus":{"properties":{"available":{"type":"boolean"},"error":{"type":"string"},"version":{"type":"string"}},"type":"object"},"contracts.ErrorResponse":{"properties":{"error":{"type":"string"},"request_id":{"type":"string"},"success":{"type":"boolean"}},"type":"object"},"contracts.GetConfigResponse":{"properties":{"config":{"description":"The configuration object","type":"object"},"config_path":{"description":"Path to config file","type":"string"}},"type":"object"},"contracts.GetRegistriesResponse":{"properties":{"registries":{"items":{"$ref":"#/components/schemas/contracts.Registry"},"type":"array","uniqueItems":false},"total":{"type":"integer"}},"type":"object"},"contracts.GetServerLogsResponse":{"properties":{"count":{"type":"integer"},"logs":{"items":{"$ref":"#/components/schemas/contracts.LogEntry"},"type":"array","uniqueItems":false},"server_name":{"type":"string"}},"type":"object"},"contracts.GetServerToolCallsResponse":{"properties":{"server_name":{"type":"string"},"tool_calls":{"items":{"$ref":"#/components/schemas/contracts.ToolCallRecord"},"type":"array","uniqueItems":false},"total":{"type":"integer"}},"type":"object"},"contracts.GetServerToolsResponse":{"properties":{"count":{"type":"integer"},"server_name":{"type":"string"},"tools":{"items":{"$ref":"#/components/schemas/contracts.Tool"},"type":"array","uniqueItems":false}},"type":"object"},"contracts.GetServersResponse":{"properties":{"servers":{"items":{"$ref":"#/components/schemas/contracts.Server"},"type":"array","uniqueItems":false},"stats":{"$ref":"#/components/schemas/contracts.ServerStats"}},"type":"object"},"contracts.GetSessionDetailResponse":{"properties":{"session":{"$ref":"#/components/schemas/contracts.MCPSession"}},"type":"object"},"contracts.GetSessionsResponse":{"properties":{"limit":{"type":"integer"},"offset":{"type":"integer"},"sessions":{"items":{"$ref":"#/components/schemas/contracts.MCPSession"},"type":"array","uniqueItems":false},"total":{"type":"integer"}},"type":"object"},"contracts.GetToolCallDetailResponse":{"properties":{"tool_call":{"$ref":"#/components/schemas/contracts.ToolCallRecord"}},"type":"object"},"contracts.GetToolCallsResponse":{"properties":{"limit":{"type":"integer"},"offset":{"type":"integer"},"tool_calls":{"items":{"$ref":"#/components/schemas/contracts.ToolCallRecord"},"type":"array","uniqueItems":false},"total":{"type":"integer"}},"type":"object"},"contracts.HealthStatus":{"description":"Unified health status calculated by the backend","properties":{"action":{"description":"Action is the suggested fix action: \"login\", \"restart\", \"enable\", \"approve\", \"view_logs\", \"set_secret\", \"configure\", or \"\" (none)","type":"string"},"admin_state":{"description":"AdminState indicates the admin state: \"enabled\", \"disabled\", or \"quarantined\"","type":"string"},"detail":{"description":"Detail is an optional longer explanation of the status","type":"string"},"level":{"description":"Level indicates the health level: \"healthy\", \"degraded\", or \"unhealthy\"","type":"string"},"summary":{"description":"Summary is a human-readable status message (e.g., \"Connected (5 tools)\")","type":"string"}},"type":"object"},"contracts.InfoEndpoints":{"description":"Available API endpoints","properties":{"http":{"description":"HTTP endpoint address (e.g., \"127.0.0.1:8080\")","type":"string"},"socket":{"description":"Unix socket path (empty if disabled)","type":"string"}},"type":"object"},"contracts.InfoResponse":{"properties":{"endpoints":{"$ref":"#/components/schemas/contracts.InfoEndpoints"},"listen_addr":{"description":"Listen address (e.g., \"127.0.0.1:8080\")","type":"string"},"update":{"$ref":"#/components/schemas/contracts.UpdateInfo"},"version":{"description":"Current MCPProxy version","type":"string"},"web_ui_url":{"description":"URL to access the web control panel","type":"string"}},"type":"object"},"contracts.IsolationConfig":{"properties":{"cpu_limit":{"type":"string"},"enabled":{"type":"boolean"},"image":{"type":"string"},"memory_limit":{"type":"string"},"timeout":{"type":"string"},"working_dir":{"type":"string"}},"type":"object"},"contracts.LogEntry":{"properties":{"fields":{"type":"object"},"level":{"type":"string"},"message":{"type":"string"},"server":{"type":"string"},"timestamp":{"type":"string"}},"type":"object"},"contracts.MCPSession":{"properties":{"client_name":{"type":"string"},"client_version":{"type":"string"},"end_time":{"type":"string"},"experimental":{"items":{"type":"string"},"type":"array","uniqueItems":false},"has_roots":{"description":"MCP Client Capabilities","type":"boolean"},"has_sampling":{"type":"boolean"},"id":{"type":"string"},"last_activity":{"type":"string"},"start_time":{"type":"string"},"status":{"type":"string"},"tool_call_count":{"type":"integer"},"total_tokens":{"type":"integer"}},"type":"object"},"contracts.MetadataStatus":{"properties":{"authorization_servers":{"items":{"type":"string"},"type":"array","uniqueItems":false},"error":{"type":"string"},"found":{"type":"boolean"},"url_checked":{"type":"string"}},"type":"object"},"contracts.MissingSecretInfo":{"properties":{"secret_name":{"type":"string"},"used_by":{"items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"contracts.NPMPackageInfo":{"properties":{"exists":{"type":"boolean"},"install_cmd":{"type":"string"}},"type":"object"},"contracts.OAuthConfig":{"properties":{"auth_url":{"type":"string"},"client_id":{"type":"string"},"extra_params":{"additionalProperties":{"type":"string"},"type":"object"},"pkce_enabled":{"type":"boolean"},"redirect_port":{"type":"integer"},"scopes":{"items":{"type":"string"},"type":"array","uniqueItems":false},"token_expires_at":{"description":"When the OAuth token expires","type":"string"},"token_url":{"type":"string"},"token_valid":{"description":"Whether token is currently valid","type":"boolean"}},"type":"object"},"contracts.OAuthErrorDetails":{"description":"Structured discovery/failure details","properties":{"authorization_server_metadata":{"$ref":"#/components/schemas/contracts.MetadataStatus"},"dcr_status":{"$ref":"#/components/schemas/contracts.DCRStatus"},"protected_resource_metadata":{"$ref":"#/components/schemas/contracts.MetadataStatus"},"server_url":{"type":"string"}},"type":"object"},"contracts.OAuthFlowError":{"properties":{"correlation_id":{"description":"Flow tracking ID for log correlation","type":"string"},"debug_hint":{"description":"CLI command for log lookup","type":"string"},"details":{"$ref":"#/components/schemas/contracts.OAuthErrorDetails"},"error_code":{"description":"Machine-readable error code (e.g., OAUTH_NO_METADATA)","type":"string"},"error_type":{"description":"Category of OAuth runtime failure","type":"string"},"message":{"description":"Human-readable error description","type":"string"},"request_id":{"description":"HTTP request ID (from PR #237)","type":"string"},"server_name":{"description":"Server that failed OAuth","type":"string"},"success":{"description":"Always false","type":"boolean"},"suggestion":{"description":"Actionable remediation hint","type":"string"}},"type":"object"},"contracts.OAuthIssue":{"properties":{"documentation_url":{"type":"string"},"error":{"type":"string"},"issue":{"type":"string"},"missing_params":{"items":{"type":"string"},"type":"array","uniqueItems":false},"resolution":{"type":"string"},"server_name":{"type":"string"}},"type":"object"},"contracts.OAuthRequirement":{"properties":{"expires_at":{"type":"string"},"message":{"type":"string"},"server_name":{"type":"string"},"state":{"type":"string"}},"type":"object"},"contracts.OAuthStartResponse":{"properties":{"auth_url":{"description":"Authorization URL (always included for manual use)","type":"string"},"browser_error":{"description":"Error message if browser launch failed","type":"string"},"browser_opened":{"description":"Whether browser launch succeeded","type":"boolean"},"correlation_id":{"description":"UUID for tracking this flow","type":"string"},"message":{"description":"Human-readable status message","type":"string"},"server_name":{"description":"Name of the server being authenticated","type":"string"},"success":{"description":"Always true for successful start","type":"boolean"}},"type":"object"},"contracts.Registry":{"properties":{"count":{"description":"number or string","type":"string"},"description":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"protocol":{"type":"string"},"servers_url":{"type":"string"},"tags":{"items":{"type":"string"},"type":"array","uniqueItems":false},"url":{"type":"string"}},"type":"object"},"contracts.ReplayToolCallRequest":{"properties":{"arguments":{"description":"Modified arguments for replay","type":"object"}},"type":"object"},"contracts.ReplayToolCallResponse":{"properties":{"error":{"description":"Error if replay failed","type":"string"},"new_call_id":{"description":"ID of the newly created call","type":"string"},"new_tool_call":{"$ref":"#/components/schemas/contracts.ToolCallRecord"},"replayed_from":{"description":"Original call ID","type":"string"},"success":{"type":"boolean"}},"type":"object"},"contracts.RepositoryInfo":{"description":"Detected package info","properties":{"npm":{"$ref":"#/components/schemas/contracts.NPMPackageInfo"}},"type":"object"},"contracts.RepositoryServer":{"properties":{"connect_url":{"description":"Alternative connection URL","type":"string"},"created_at":{"type":"string"},"description":{"type":"string"},"id":{"type":"string"},"install_cmd":{"description":"Installation command","type":"string"},"name":{"type":"string"},"registry":{"description":"Which registry this came from","type":"string"},"repository_info":{"$ref":"#/components/schemas/contracts.RepositoryInfo"},"source_code_url":{"description":"Source repository URL","type":"string"},"updated_at":{"type":"string"},"url":{"description":"MCP endpoint for remote servers only","type":"string"}},"type":"object"},"contracts.SearchRegistryServersResponse":{"properties":{"query":{"type":"string"},"registry_id":{"type":"string"},"servers":{"items":{"$ref":"#/components/schemas/contracts.RepositoryServer"},"type":"array","uniqueItems":false},"tag":{"type":"string"},"total":{"type":"integer"}},"type":"object"},"contracts.SearchResult":{"properties":{"matches":{"type":"integer"},"score":{"type":"number"},"snippet":{"type":"string"},"tool":{"$ref":"#/components/schemas/contracts.Tool"}},"type":"object"},"contracts.SearchToolsResponse":{"properties":{"query":{"type":"string"},"results":{"items":{"$ref":"#/components/schemas/contracts.SearchResult"},"type":"array","uniqueItems":false},"took":{"type":"string"},"total":{"type":"integer"}},"type":"object"},"contracts.Server":{"properties":{"args":{"items":{"type":"string"},"type":"array","uniqueItems":false},"authenticated":{"description":"OAuth authentication status","type":"boolean"},"command":{"type":"string"},"connected":{"type":"boolean"},"connected_at":{"type":"string"},"connecting":{"type":"boolean"},"created":{"type":"string"},"enabled":{"type":"boolean"},"env":{"additionalProperties":{"type":"string"},"type":"object"},"headers":{"additionalProperties":{"type":"string"},"type":"object"},"health":{"$ref":"#/components/schemas/contracts.HealthStatus"},"id":{"type":"string"},"isolation":{"$ref":"#/components/schemas/contracts.IsolationConfig"},"last_error":{"type":"string"},"last_reconnect_at":{"type":"string"},"last_retry_time":{"type":"string"},"name":{"type":"string"},"oauth":{"$ref":"#/components/schemas/contracts.OAuthConfig"},"oauth_status":{"description":"OAuth status: \"authenticated\", \"expired\", \"error\", \"none\"","type":"string"},"protocol":{"type":"string"},"quarantined":{"type":"boolean"},"reconnect_count":{"type":"integer"},"retry_count":{"type":"integer"},"should_retry":{"type":"boolean"},"status":{"type":"string"},"token_expires_at":{"description":"When the OAuth token expires (ISO 8601)","type":"string"},"tool_count":{"type":"integer"},"tool_list_token_size":{"description":"Token size for this server's tools","type":"integer"},"updated":{"type":"string"},"url":{"type":"string"},"user_logged_out":{"description":"True if user explicitly logged out (prevents auto-reconnection)","type":"boolean"},"working_dir":{"type":"string"}},"type":"object"},"contracts.ServerActionResponse":{"properties":{"action":{"type":"string"},"async":{"type":"boolean"},"server":{"type":"string"},"success":{"type":"boolean"}},"type":"object"},"contracts.ServerStats":{"properties":{"connected_servers":{"type":"integer"},"docker_containers":{"type":"integer"},"quarantined_servers":{"type":"integer"},"token_metrics":{"$ref":"#/components/schemas/contracts.ServerTokenMetrics"},"total_servers":{"type":"integer"},"total_tools":{"type":"integer"}},"type":"object"},"contracts.ServerTokenMetrics":{"properties":{"average_query_result_size":{"description":"Typical retrieve_tools output (tokens)","type":"integer"},"per_server_tool_list_sizes":{"additionalProperties":{"type":"integer"},"description":"Token size per server","type":"object"},"saved_tokens":{"description":"Difference","type":"integer"},"saved_tokens_percentage":{"description":"Percentage saved","type":"number"},"total_server_tool_list_size":{"description":"All upstream tools combined (tokens)","type":"integer"}},"type":"object"},"contracts.SuccessResponse":{"properties":{"data":{"type":"object"},"success":{"type":"boolean"}},"type":"object"},"contracts.TokenMetrics":{"description":"Token usage metrics (nil for older records)","properties":{"encoding":{"description":"Encoding used (e.g., cl100k_base)","type":"string"},"estimated_cost":{"description":"Optional cost estimate","type":"number"},"input_tokens":{"description":"Tokens in the request","type":"integer"},"model":{"description":"Model used for tokenization","type":"string"},"output_tokens":{"description":"Tokens in the response","type":"integer"},"total_tokens":{"description":"Total tokens (input + output)","type":"integer"},"truncated_tokens":{"description":"Tokens removed by truncation","type":"integer"},"was_truncated":{"description":"Whether response was truncated","type":"boolean"}},"type":"object"},"contracts.Tool":{"properties":{"annotations":{"$ref":"#/components/schemas/contracts.ToolAnnotation"},"description":{"type":"string"},"last_used":{"type":"string"},"name":{"type":"string"},"schema":{"type":"object"},"server_name":{"type":"string"},"usage":{"type":"integer"}},"type":"object"},"contracts.ToolAnnotation":{"description":"Tool behavior hints snapshot","properties":{"destructiveHint":{"type":"boolean"},"idempotentHint":{"type":"boolean"},"openWorldHint":{"type":"boolean"},"readOnlyHint":{"type":"boolean"},"title":{"type":"string"}},"type":"object"},"contracts.ToolCallRecord":{"description":"The new tool call record","properties":{"annotations":{"$ref":"#/components/schemas/contracts.ToolAnnotation"},"arguments":{"description":"Tool arguments","type":"object"},"config_path":{"description":"Active config file path","type":"string"},"duration":{"description":"Duration in nanoseconds","type":"integer"},"error":{"description":"Error message (failure only)","type":"string"},"execution_type":{"description":"\"direct\" or \"code_execution\"","type":"string"},"id":{"description":"Unique identifier","type":"string"},"mcp_client_name":{"description":"MCP client name from InitializeRequest","type":"string"},"mcp_client_version":{"description":"MCP client version","type":"string"},"mcp_session_id":{"description":"MCP session identifier","type":"string"},"metrics":{"$ref":"#/components/schemas/contracts.TokenMetrics"},"parent_call_id":{"description":"Links nested calls to parent code_execution","type":"string"},"request_id":{"description":"Request correlation ID","type":"string"},"response":{"description":"Tool response (success only)","type":"object"},"server_id":{"description":"Server identity hash","type":"string"},"server_name":{"description":"Human-readable server name","type":"string"},"timestamp":{"description":"When the call was made","type":"string"},"tool_name":{"description":"Tool name (without server prefix)","type":"string"}},"type":"object"},"contracts.UpdateInfo":{"description":"Update information (if available)","properties":{"available":{"description":"Whether an update is available","type":"boolean"},"check_error":{"description":"Error message if update check failed","type":"string"},"checked_at":{"description":"When the update check was performed","type":"string"},"is_prerelease":{"description":"Whether the latest version is a prerelease","type":"boolean"},"latest_version":{"description":"Latest version available (e.g., \"v1.2.3\")","type":"string"},"release_url":{"description":"URL to the release page","type":"string"}},"type":"object"},"contracts.UpstreamError":{"properties":{"error_message":{"type":"string"},"server_name":{"type":"string"},"timestamp":{"type":"string"}},"type":"object"},"contracts.ValidateConfigResponse":{"properties":{"errors":{"items":{"$ref":"#/components/schemas/contracts.ValidationError"},"type":"array","uniqueItems":false},"valid":{"type":"boolean"}},"type":"object"},"contracts.ValidationError":{"properties":{"field":{"type":"string"},"message":{"type":"string"}},"type":"object"},"data":{"properties":{"data":{"$ref":"#/components/schemas/contracts.InfoResponse"}},"type":"object"},"httpapi.AddServerRequest":{"properties":{"args":{"items":{"type":"string"},"type":"array","uniqueItems":false},"command":{"type":"string"},"enabled":{"type":"boolean"},"env":{"additionalProperties":{"type":"string"},"type":"object"},"headers":{"additionalProperties":{"type":"string"},"type":"object"},"name":{"type":"string"},"protocol":{"type":"string"},"quarantined":{"type":"boolean"},"url":{"type":"string"},"working_dir":{"type":"string"}},"type":"object"},"management.BulkOperationResult":{"properties":{"errors":{"additionalProperties":{"type":"string"},"description":"Map of server name to error message","type":"object"},"failed":{"description":"Number of failed operations","type":"integer"},"successful":{"description":"Number of successful operations","type":"integer"},"total":{"description":"Total servers processed","type":"integer"}},"type":"object"},"observability.HealthResponse":{"properties":{"components":{"items":{"$ref":"#/components/schemas/observability.HealthStatus"},"type":"array","uniqueItems":false},"status":{"description":"\"healthy\" or \"unhealthy\"","type":"string"},"timestamp":{"type":"string"}},"type":"object"},"observability.HealthStatus":{"properties":{"error":{"type":"string"},"latency":{"type":"string"},"name":{"type":"string"},"status":{"description":"\"healthy\" or \"unhealthy\"","type":"string"}},"type":"object"},"observability.ReadinessResponse":{"properties":{"components":{"items":{"$ref":"#/components/schemas/observability.HealthStatus"},"type":"array","uniqueItems":false},"status":{"description":"\"ready\" or \"not_ready\"","type":"string"},"timestamp":{"type":"string"}},"type":"object"},"secureenv.EnvConfig":{"description":"Environment configuration for secure variable filtering","properties":{"allowed_system_vars":{"items":{"type":"string"},"type":"array","uniqueItems":false},"custom_vars":{"additionalProperties":{"type":"string"},"type":"object"},"enhance_path":{"description":"Enable PATH enhancement for Launchd scenarios","type":"boolean"},"inherit_system_safe":{"type":"boolean"}},"type":"object"}},"securitySchemes":{"ApiKeyAuth":{"description":"API key authentication via query parameter. Use ?apikey=your-key","in":"query","name":"apikey","type":"apiKey"}}},
"info": {"contact":{"name":"MCPProxy Support","url":"https://github.com/smart-mcp-proxy/mcpproxy-go"},"description":"{{escape .Description}}","license":{"name":"MIT","url":"https://opensource.org/licenses/MIT"},"title":"{{.Title}}","version":"{{.Version}}"},
"externalDocs": {"description":"","url":""},
- "paths": {"/api/v1/activity":{"get":{"description":"Returns paginated list of activity records with optional filtering","parameters":[{"description":"Filter by activity type","in":"query","name":"type","schema":{"enum":["tool_call","policy_decision","quarantine_change","server_change"],"type":"string"}},{"description":"Filter by server name","in":"query","name":"server","schema":{"type":"string"}},{"description":"Filter by tool name","in":"query","name":"tool","schema":{"type":"string"}},{"description":"Filter by MCP session ID","in":"query","name":"session_id","schema":{"type":"string"}},{"description":"Filter by status","in":"query","name":"status","schema":{"enum":["success","error","blocked"],"type":"string"}},{"description":"Filter by intent operation type (Spec 018)","in":"query","name":"intent_type","schema":{"enum":["read","write","destructive"],"type":"string"}},{"description":"Filter by HTTP request ID for log correlation (Spec 021)","in":"query","name":"request_id","schema":{"type":"string"}},{"description":"Filter activities after this time (RFC3339)","in":"query","name":"start_time","schema":{"type":"string"}},{"description":"Filter activities before this time (RFC3339)","in":"query","name":"end_time","schema":{"type":"string"}},{"description":"Maximum records to return (1-100, default 50)","in":"query","name":"limit","schema":{"type":"integer"}},{"description":"Pagination offset (default 0)","in":"query","name":"offset","schema":{"type":"integer"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/data"}],"properties":{"data":{"type":"object"},"error":{"type":"string"},"request_id":{"type":"string"},"success":{"type":"boolean"}},"type":"object"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.APIResponse"}}},"description":"Bad Request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.APIResponse"}}},"description":"Unauthorized"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.APIResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyHeader":[]},{"ApiKeyQuery":[]}],"summary":"List activity records","tags":["Activity"]}},"/api/v1/activity/export":{"get":{"description":"Exports activity records in JSON Lines or CSV format for compliance","parameters":[{"description":"Export format: json (default) or csv","in":"query","name":"format","schema":{"type":"string"}},{"description":"Filter by activity type","in":"query","name":"type","schema":{"type":"string"}},{"description":"Filter by server name","in":"query","name":"server","schema":{"type":"string"}},{"description":"Filter by tool name","in":"query","name":"tool","schema":{"type":"string"}},{"description":"Filter by MCP session ID","in":"query","name":"session_id","schema":{"type":"string"}},{"description":"Filter by status","in":"query","name":"status","schema":{"type":"string"}},{"description":"Filter activities after this time (RFC3339)","in":"query","name":"start_time","schema":{"type":"string"}},{"description":"Filter activities before this time (RFC3339)","in":"query","name":"end_time","schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"type":"string"}},"application/x-ndjson":{"schema":{"type":"string"}},"text/csv":{"schema":{"type":"string"}}},"description":"Streamed activity records"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.APIResponse"}}},"description":"Unauthorized"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.APIResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyHeader":[]},{"ApiKeyQuery":[]}],"summary":"Export activity records","tags":["Activity"]}},"/api/v1/activity/summary":{"get":{"description":"Returns aggregated activity statistics for a time period","parameters":[{"description":"Time period: 1h, 24h (default), 7d, 30d","in":"query","name":"period","schema":{"type":"string"}},{"description":"Group by: server, tool (optional)","in":"query","name":"group_by","schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/data"}],"properties":{"data":{"type":"object"},"error":{"type":"string"},"request_id":{"type":"string"},"success":{"type":"boolean"}},"type":"object"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.APIResponse"}}},"description":"Bad Request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.APIResponse"}}},"description":"Unauthorized"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.APIResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyHeader":[]},{"ApiKeyQuery":[]}],"summary":"Get activity summary statistics","tags":["Activity"]}},"/api/v1/activity/{id}":{"get":{"description":"Returns full details for a single activity record","parameters":[{"description":"Activity record ID (ULID)","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/data"}],"properties":{"data":{"type":"object"},"error":{"type":"string"},"request_id":{"type":"string"},"success":{"type":"boolean"}},"type":"object"}}},"description":"OK"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.APIResponse"}}},"description":"Unauthorized"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.APIResponse"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.APIResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyHeader":[]},{"ApiKeyQuery":[]}],"summary":"Get activity record details","tags":["Activity"]}},"/api/v1/config":{"get":{"description":"Retrieves the current MCPProxy configuration including all server definitions, global settings, and runtime parameters","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.GetConfigResponse"}}},"description":"Configuration retrieved successfully"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Unauthorized - missing or invalid API key"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Failed to get configuration"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Get current configuration","tags":["config"]}},"/api/v1/config/apply":{"post":{"description":"Applies a new MCPProxy configuration. Validates and persists the configuration to disk. Some changes apply immediately, while others may require a restart. Returns detailed information about applied changes and restart requirements.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/config.Config"}}},"description":"Configuration to apply","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ConfigApplyResult"}}},"description":"Configuration applied successfully with change details"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Invalid JSON payload"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Unauthorized - missing or invalid API key"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Failed to apply configuration"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Apply configuration","tags":["config"]}},"/api/v1/config/validate":{"post":{"description":"Validates a provided MCPProxy configuration without applying it. Checks for syntax errors, invalid server definitions, conflicting settings, and other configuration issues.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/config.Config"}}},"description":"Configuration to validate","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ValidateConfigResponse"}}},"description":"Configuration validation result"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Invalid JSON payload"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Unauthorized - missing or invalid API key"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Validation failed"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Validate configuration","tags":["config"]}},"/api/v1/diagnostics":{"get":{"description":"Get comprehensive health diagnostics including upstream errors, OAuth requirements, missing secrets, and Docker status","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.Diagnostics"}}},"description":"Health diagnostics"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Get health diagnostics","tags":["diagnostics"]}},"/api/v1/docker/status":{"get":{"description":"Retrieve current Docker availability and recovery status","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.SuccessResponse"}}},"description":"Docker status information"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Get Docker status","tags":["docker"]}},"/api/v1/doctor":{"get":{"description":"Get comprehensive health diagnostics including upstream errors, OAuth requirements, missing secrets, and Docker status","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.Diagnostics"}}},"description":"Health diagnostics"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Get health diagnostics","tags":["diagnostics"]}},"/api/v1/index/search":{"get":{"description":"Search across all upstream MCP server tools using BM25 keyword search","parameters":[{"description":"Search query","in":"query","name":"q","required":true,"schema":{"type":"string"}},{"description":"Maximum number of results","in":"query","name":"limit","schema":{"default":10,"maximum":100,"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.SearchToolsResponse"}}},"description":"Search results"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Bad request (missing query parameter)"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Search for tools","tags":["tools"]}},"/api/v1/info":{"get":{"description":"Get essential server metadata including version, web UI URL, endpoint addresses, and update availability\nThis endpoint is designed for tray-core communication and version checking\nUse refresh=true query parameter to force an immediate update check against GitHub","parameters":[{"description":"Force immediate update check against GitHub","in":"query","name":"refresh","schema":{"type":"boolean"}}],"responses":{"200":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/data"}],"properties":{"data":{"type":"object"},"error":{"type":"string"},"request_id":{"type":"string"},"success":{"type":"boolean"}},"type":"object"}}},"description":"Server information with optional update info"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Get server information","tags":["status"]}},"/api/v1/registries":{"get":{"description":"Retrieves list of all MCP server registries that can be browsed for discovering and installing new upstream servers. Includes registry metadata, server counts, and API endpoints.","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.GetRegistriesResponse"}}},"description":"Registries retrieved successfully"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Unauthorized - missing or invalid API key"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Failed to list registries"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"List available MCP server registries","tags":["registries"]}},"/api/v1/registries/{id}/servers":{"get":{"description":"Searches for MCP servers within a specific registry by keyword or tag. Returns server metadata including installation commands, source code URLs, and npm package information for easy discovery and installation.","parameters":[{"description":"Registry ID","in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"Search query keyword","in":"query","name":"q","schema":{"type":"string"}},{"description":"Filter by tag","in":"query","name":"tag","schema":{"type":"string"}},{"description":"Maximum number of results (default 10)","in":"query","name":"limit","schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.SearchRegistryServersResponse"}}},"description":"Servers retrieved successfully"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Registry ID required"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Unauthorized - missing or invalid API key"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Failed to search servers"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Search MCP servers in a registry","tags":["registries"]}},"/api/v1/secrets":{"post":{"description":"Stores a secret value in the operating system's secure keyring. The secret can then be referenced in configuration using ${keyring:secret-name} syntax. Automatically notifies runtime to restart affected servers.","requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"additionalProperties":{},"type":"object"}}},"description":"Secret stored successfully with reference syntax"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Invalid JSON payload, missing name/value, or unsupported type"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Unauthorized - missing or invalid API key"},"405":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Method not allowed"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Secret resolver not available or failed to store secret"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Store a secret in OS keyring","tags":["secrets"]}},"/api/v1/secrets/{name}":{"delete":{"description":"Deletes a secret from the operating system's secure keyring. Automatically notifies runtime to restart affected servers. Only keyring type is supported for security.","parameters":[{"description":"Name of the secret to delete","in":"path","name":"name","required":true,"schema":{"type":"string"}},{"description":"Secret type (only 'keyring' supported, defaults to 'keyring')","in":"query","name":"type","schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"additionalProperties":{},"type":"object"}}},"description":"Secret deleted successfully"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Missing secret name or unsupported type"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Unauthorized - missing or invalid API key"},"405":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Method not allowed"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Secret resolver not available or failed to delete secret"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Delete a secret from OS keyring","tags":["secrets"]}},"/api/v1/servers":{"get":{"description":"Get a list of all configured upstream MCP servers with their connection status and statistics","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.GetServersResponse"}}},"description":"Server list with statistics"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"List all upstream MCP servers","tags":["servers"]},"post":{"description":"Add a new MCP upstream server to the configuration. New servers are quarantined by default for security.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/httpapi.AddServerRequest"}}},"description":"Server configuration","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ServerActionResponse"}}},"description":"Server added successfully"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Bad request - invalid configuration"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Conflict - server with this name already exists"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Add a new upstream server","tags":["servers"]}},"/api/v1/servers/disable_all":{"post":{"description":"Disable all configured upstream MCP servers with partial failure handling","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/management.BulkOperationResult"}}},"description":"Bulk disable results with success/failure counts"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Forbidden (management disabled)"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Disable all servers","tags":["servers"]}},"/api/v1/servers/enable_all":{"post":{"description":"Enable all configured upstream MCP servers with partial failure handling","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/management.BulkOperationResult"}}},"description":"Bulk enable results with success/failure counts"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Forbidden (management disabled)"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Enable all servers","tags":["servers"]}},"/api/v1/servers/reconnect":{"post":{"description":"Force reconnection to all upstream MCP servers","parameters":[{"description":"Reason for reconnection","in":"query","name":"reason","schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ServerActionResponse"}}},"description":"All servers reconnected successfully"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Reconnect all servers","tags":["servers"]}},"/api/v1/servers/restart_all":{"post":{"description":"Restart all configured upstream MCP servers sequentially with partial failure handling","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/management.BulkOperationResult"}}},"description":"Bulk restart results with success/failure counts"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Forbidden (management disabled)"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Restart all servers","tags":["servers"]}},"/api/v1/servers/{id}":{"delete":{"description":"Remove an MCP upstream server from the configuration. This stops the server if running and removes it from config.","parameters":[{"description":"Server ID or name","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ServerActionResponse"}}},"description":"Server removed successfully"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Bad request"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Server not found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Remove an upstream server","tags":["servers"]}},"/api/v1/servers/{id}/disable":{"post":{"description":"Disable a specific upstream MCP server","parameters":[{"description":"Server ID or name","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ServerActionResponse"}}},"description":"Server disabled successfully"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Bad request"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Server not found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Disable an upstream server","tags":["servers"]}},"/api/v1/servers/{id}/discover-tools":{"post":{"description":"Manually trigger tool discovery and indexing for a specific upstream MCP server. This forces an immediate refresh of the server's tool cache.","parameters":[{"description":"Server ID or name","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ServerActionResponse"}}},"description":"Tool discovery triggered successfully"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Bad request (missing server ID)"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Server not found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Failed to discover tools"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Discover tools for a specific server","tags":["servers"]}},"/api/v1/servers/{id}/enable":{"post":{"description":"Enable a specific upstream MCP server","parameters":[{"description":"Server ID or name","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ServerActionResponse"}}},"description":"Server enabled successfully"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Bad request"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Server not found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Enable an upstream server","tags":["servers"]}},"/api/v1/servers/{id}/login":{"post":{"description":"Initiate OAuth authentication flow for a specific upstream MCP server. Returns structured OAuth start response with correlation ID for tracking.","parameters":[{"description":"Server ID or name","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.OAuthStartResponse"}}},"description":"OAuth login initiated successfully"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.OAuthFlowError"}}},"description":"OAuth error (client_id required, DCR failed, etc.)"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Server not found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Trigger OAuth login for server","tags":["servers"]}},"/api/v1/servers/{id}/logout":{"post":{"description":"Clear OAuth authentication token and disconnect a specific upstream MCP server. The server will need to re-authenticate before tools can be used again.","parameters":[{"description":"Server ID or name","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ServerActionResponse"}}},"description":"OAuth logout completed successfully"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Bad request (missing server ID)"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Forbidden (management disabled or read-only mode)"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Server not found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Clear OAuth token and disconnect server","tags":["servers"]}},"/api/v1/servers/{id}/logs":{"get":{"description":"Retrieve log entries for a specific upstream MCP server","parameters":[{"description":"Server ID or name","in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"Number of log lines to retrieve","in":"query","name":"tail","schema":{"default":100,"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.GetServerLogsResponse"}}},"description":"Server logs retrieved successfully"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Bad request (missing server ID)"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Server not found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Get server logs","tags":["servers"]}},"/api/v1/servers/{id}/quarantine":{"post":{"description":"Place a specific upstream MCP server in quarantine to prevent tool execution","parameters":[{"description":"Server ID or name","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ServerActionResponse"}}},"description":"Server quarantined successfully"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Bad request (missing server ID)"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Server not found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Quarantine a server","tags":["servers"]}},"/api/v1/servers/{id}/restart":{"post":{"description":"Restart the connection to a specific upstream MCP server","parameters":[{"description":"Server ID or name","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ServerActionResponse"}}},"description":"Server restarted successfully"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Bad request"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Server not found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Restart an upstream server","tags":["servers"]}},"/api/v1/servers/{id}/tool-calls":{"get":{"description":"Retrieves tool call history filtered by upstream server ID. Returns recent tool executions for the specified server including timestamps, arguments, results, and errors. Useful for server-specific debugging and monitoring.","parameters":[{"description":"Upstream server ID or name","in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"Maximum number of records to return (1-100, default 50)","in":"query","name":"limit","schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.GetServerToolCallsResponse"}}},"description":"Server tool calls retrieved successfully"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Server ID required"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Unauthorized - missing or invalid API key"},"405":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Method not allowed"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Failed to get server tool calls"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Get tool call history for specific server","tags":["tool-calls"]}},"/api/v1/servers/{id}/tools":{"get":{"description":"Retrieve all available tools for a specific upstream MCP server","parameters":[{"description":"Server ID or name","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.GetServerToolsResponse"}}},"description":"Server tools retrieved successfully"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Bad request (missing server ID)"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Server not found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Get tools for a server","tags":["servers"]}},"/api/v1/servers/{id}/unquarantine":{"post":{"description":"Remove a specific upstream MCP server from quarantine to allow tool execution","parameters":[{"description":"Server ID or name","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ServerActionResponse"}}},"description":"Server unquarantined successfully"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Bad request (missing server ID)"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Server not found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Unquarantine a server","tags":["servers"]}},"/api/v1/sessions":{"get":{"description":"Retrieves paginated list of active and recent MCP client sessions. Each session represents a connection from an MCP client to MCPProxy, tracking initialization time, tool calls, and connection status.","parameters":[{"description":"Maximum number of sessions to return (1-100, default 10)","in":"query","name":"limit","schema":{"type":"integer"}},{"description":"Number of sessions to skip for pagination (default 0)","in":"query","name":"offset","schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.GetSessionsResponse"}}},"description":"Sessions retrieved successfully"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Unauthorized - missing or invalid API key"},"405":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Method not allowed"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Failed to get sessions"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Get active MCP sessions","tags":["sessions"]}},"/api/v1/sessions/{id}":{"get":{"description":"Retrieves detailed information about a specific MCP client session including initialization parameters, connection status, tool call count, and activity timestamps.","parameters":[{"description":"Session ID","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.GetSessionDetailResponse"}}},"description":"Session details retrieved successfully"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Session ID required"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Unauthorized - missing or invalid API key"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Session not found"},"405":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Method not allowed"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Get MCP session details by ID","tags":["sessions"]}},"/api/v1/stats/tokens":{"get":{"description":"Retrieve token savings statistics across all servers and sessions","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.SuccessResponse"}}},"description":"Token statistics"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Get token savings statistics","tags":["stats"]}},"/api/v1/status":{"get":{"description":"Get comprehensive server status including running state, listen address, upstream statistics, and timestamp","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.SuccessResponse"}}},"description":"Server status information"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Get server status","tags":["status"]}},"/api/v1/tool-calls":{"get":{"description":"Retrieves paginated tool call history across all upstream servers or filtered by session ID. Includes execution timestamps, arguments, results, and error information for debugging and auditing.","parameters":[{"description":"Maximum number of records to return (1-100, default 50)","in":"query","name":"limit","schema":{"type":"integer"}},{"description":"Number of records to skip for pagination (default 0)","in":"query","name":"offset","schema":{"type":"integer"}},{"description":"Filter tool calls by MCP session ID","in":"query","name":"session_id","schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.GetToolCallsResponse"}}},"description":"Tool calls retrieved successfully"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Unauthorized - missing or invalid API key"},"405":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Method not allowed"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Failed to get tool calls"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Get tool call history","tags":["tool-calls"]}},"/api/v1/tool-calls/{id}":{"get":{"description":"Retrieves detailed information about a specific tool call execution including full request arguments, response data, execution time, and any errors encountered.","parameters":[{"description":"Tool call ID","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.GetToolCallDetailResponse"}}},"description":"Tool call details retrieved successfully"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Tool call ID required"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Unauthorized - missing or invalid API key"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Tool call not found"},"405":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Method not allowed"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Get tool call details by ID","tags":["tool-calls"]}},"/api/v1/tool-calls/{id}/replay":{"post":{"description":"Re-executes a previous tool call with optional modified arguments. Useful for debugging and testing tool behavior with different inputs. Creates a new tool call record linked to the original.","parameters":[{"description":"Original tool call ID to replay","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ReplayToolCallRequest"}}},"description":"Optional modified arguments for replay"},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ReplayToolCallResponse"}}},"description":"Tool call replayed successfully"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Tool call ID required or invalid JSON payload"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Unauthorized - missing or invalid API key"},"405":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Method not allowed"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Failed to replay tool call"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Replay a tool call","tags":["tool-calls"]}},"/api/v1/tools/call":{"post":{"description":"Execute a tool on an upstream MCP server (wrapper around MCP tool calls)","requestBody":{"content":{"application/json":{"schema":{"properties":{"arguments":{"type":"object"},"tool_name":{"type":"string"}},"type":"object"}}},"description":"Tool call request with tool name and arguments","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.SuccessResponse"}}},"description":"Tool call result"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Bad request (invalid payload or missing tool name)"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error or tool execution failure"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Call a tool","tags":["tools"]}},"/healthz":{"get":{"description":"Get comprehensive health status including all component health (Kubernetes-compatible liveness probe)","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/observability.HealthResponse"}}},"description":"Service is healthy"},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/observability.HealthResponse"}}},"description":"Service is unhealthy"}},"summary":"Get health status","tags":["health"]}},"/readyz":{"get":{"description":"Get readiness status including all component readiness checks (Kubernetes-compatible readiness probe)","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/observability.ReadinessResponse"}}},"description":"Service is ready"},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/observability.ReadinessResponse"}}},"description":"Service is not ready"}},"summary":"Get readiness status","tags":["health"]}}},
+ "paths": {"/api/v1/activity":{"get":{"description":"Returns paginated list of activity records with optional filtering","parameters":[{"description":"Filter by activity type(s), comma-separated for multiple (Spec 024)","in":"query","name":"type","schema":{"enum":["tool_call","policy_decision","quarantine_change","server_change","system_start","system_stop","internal_tool_call","config_change"],"type":"string"}},{"description":"Filter by server name","in":"query","name":"server","schema":{"type":"string"}},{"description":"Filter by tool name","in":"query","name":"tool","schema":{"type":"string"}},{"description":"Filter by MCP session ID","in":"query","name":"session_id","schema":{"type":"string"}},{"description":"Filter by status","in":"query","name":"status","schema":{"enum":["success","error","blocked"],"type":"string"}},{"description":"Filter by intent operation type (Spec 018)","in":"query","name":"intent_type","schema":{"enum":["read","write","destructive"],"type":"string"}},{"description":"Filter by HTTP request ID for log correlation (Spec 021)","in":"query","name":"request_id","schema":{"type":"string"}},{"description":"Include successful call_tool_* internal tool calls (default: false, excluded to avoid duplicates)","in":"query","name":"include_call_tool","schema":{"type":"boolean"}},{"description":"Filter activities after this time (RFC3339)","in":"query","name":"start_time","schema":{"type":"string"}},{"description":"Filter activities before this time (RFC3339)","in":"query","name":"end_time","schema":{"type":"string"}},{"description":"Maximum records to return (1-100, default 50)","in":"query","name":"limit","schema":{"type":"integer"}},{"description":"Pagination offset (default 0)","in":"query","name":"offset","schema":{"type":"integer"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/data"}],"properties":{"data":{"type":"object"},"error":{"type":"string"},"request_id":{"type":"string"},"success":{"type":"boolean"}},"type":"object"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.APIResponse"}}},"description":"Bad Request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.APIResponse"}}},"description":"Unauthorized"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.APIResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyHeader":[]},{"ApiKeyQuery":[]}],"summary":"List activity records","tags":["Activity"]}},"/api/v1/activity/export":{"get":{"description":"Exports activity records in JSON Lines or CSV format for compliance","parameters":[{"description":"Export format: json (default) or csv","in":"query","name":"format","schema":{"type":"string"}},{"description":"Filter by activity type","in":"query","name":"type","schema":{"type":"string"}},{"description":"Filter by server name","in":"query","name":"server","schema":{"type":"string"}},{"description":"Filter by tool name","in":"query","name":"tool","schema":{"type":"string"}},{"description":"Filter by MCP session ID","in":"query","name":"session_id","schema":{"type":"string"}},{"description":"Filter by status","in":"query","name":"status","schema":{"type":"string"}},{"description":"Filter activities after this time (RFC3339)","in":"query","name":"start_time","schema":{"type":"string"}},{"description":"Filter activities before this time (RFC3339)","in":"query","name":"end_time","schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"type":"string"}},"application/x-ndjson":{"schema":{"type":"string"}},"text/csv":{"schema":{"type":"string"}}},"description":"Streamed activity records"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.APIResponse"}}},"description":"Unauthorized"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.APIResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyHeader":[]},{"ApiKeyQuery":[]}],"summary":"Export activity records","tags":["Activity"]}},"/api/v1/activity/summary":{"get":{"description":"Returns aggregated activity statistics for a time period","parameters":[{"description":"Time period: 1h, 24h (default), 7d, 30d","in":"query","name":"period","schema":{"type":"string"}},{"description":"Group by: server, tool (optional)","in":"query","name":"group_by","schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/data"}],"properties":{"data":{"type":"object"},"error":{"type":"string"},"request_id":{"type":"string"},"success":{"type":"boolean"}},"type":"object"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.APIResponse"}}},"description":"Bad Request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.APIResponse"}}},"description":"Unauthorized"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.APIResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyHeader":[]},{"ApiKeyQuery":[]}],"summary":"Get activity summary statistics","tags":["Activity"]}},"/api/v1/activity/{id}":{"get":{"description":"Returns full details for a single activity record","parameters":[{"description":"Activity record ID (ULID)","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/data"}],"properties":{"data":{"type":"object"},"error":{"type":"string"},"request_id":{"type":"string"},"success":{"type":"boolean"}},"type":"object"}}},"description":"OK"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.APIResponse"}}},"description":"Unauthorized"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.APIResponse"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.APIResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyHeader":[]},{"ApiKeyQuery":[]}],"summary":"Get activity record details","tags":["Activity"]}},"/api/v1/config":{"get":{"description":"Retrieves the current MCPProxy configuration including all server definitions, global settings, and runtime parameters","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.GetConfigResponse"}}},"description":"Configuration retrieved successfully"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Unauthorized - missing or invalid API key"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Failed to get configuration"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Get current configuration","tags":["config"]}},"/api/v1/config/apply":{"post":{"description":"Applies a new MCPProxy configuration. Validates and persists the configuration to disk. Some changes apply immediately, while others may require a restart. Returns detailed information about applied changes and restart requirements.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/config.Config"}}},"description":"Configuration to apply","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ConfigApplyResult"}}},"description":"Configuration applied successfully with change details"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Invalid JSON payload"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Unauthorized - missing or invalid API key"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Failed to apply configuration"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Apply configuration","tags":["config"]}},"/api/v1/config/validate":{"post":{"description":"Validates a provided MCPProxy configuration without applying it. Checks for syntax errors, invalid server definitions, conflicting settings, and other configuration issues.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/config.Config"}}},"description":"Configuration to validate","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ValidateConfigResponse"}}},"description":"Configuration validation result"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Invalid JSON payload"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Unauthorized - missing or invalid API key"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Validation failed"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Validate configuration","tags":["config"]}},"/api/v1/diagnostics":{"get":{"description":"Get comprehensive health diagnostics including upstream errors, OAuth requirements, missing secrets, and Docker status","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.Diagnostics"}}},"description":"Health diagnostics"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Get health diagnostics","tags":["diagnostics"]}},"/api/v1/docker/status":{"get":{"description":"Retrieve current Docker availability and recovery status","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.SuccessResponse"}}},"description":"Docker status information"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Get Docker status","tags":["docker"]}},"/api/v1/doctor":{"get":{"description":"Get comprehensive health diagnostics including upstream errors, OAuth requirements, missing secrets, and Docker status","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.Diagnostics"}}},"description":"Health diagnostics"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Get health diagnostics","tags":["diagnostics"]}},"/api/v1/index/search":{"get":{"description":"Search across all upstream MCP server tools using BM25 keyword search","parameters":[{"description":"Search query","in":"query","name":"q","required":true,"schema":{"type":"string"}},{"description":"Maximum number of results","in":"query","name":"limit","schema":{"default":10,"maximum":100,"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.SearchToolsResponse"}}},"description":"Search results"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Bad request (missing query parameter)"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Search for tools","tags":["tools"]}},"/api/v1/info":{"get":{"description":"Get essential server metadata including version, web UI URL, endpoint addresses, and update availability\nThis endpoint is designed for tray-core communication and version checking\nUse refresh=true query parameter to force an immediate update check against GitHub","parameters":[{"description":"Force immediate update check against GitHub","in":"query","name":"refresh","schema":{"type":"boolean"}}],"responses":{"200":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/data"}],"properties":{"data":{"type":"object"},"error":{"type":"string"},"request_id":{"type":"string"},"success":{"type":"boolean"}},"type":"object"}}},"description":"Server information with optional update info"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Get server information","tags":["status"]}},"/api/v1/registries":{"get":{"description":"Retrieves list of all MCP server registries that can be browsed for discovering and installing new upstream servers. Includes registry metadata, server counts, and API endpoints.","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.GetRegistriesResponse"}}},"description":"Registries retrieved successfully"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Unauthorized - missing or invalid API key"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Failed to list registries"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"List available MCP server registries","tags":["registries"]}},"/api/v1/registries/{id}/servers":{"get":{"description":"Searches for MCP servers within a specific registry by keyword or tag. Returns server metadata including installation commands, source code URLs, and npm package information for easy discovery and installation.","parameters":[{"description":"Registry ID","in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"Search query keyword","in":"query","name":"q","schema":{"type":"string"}},{"description":"Filter by tag","in":"query","name":"tag","schema":{"type":"string"}},{"description":"Maximum number of results (default 10)","in":"query","name":"limit","schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.SearchRegistryServersResponse"}}},"description":"Servers retrieved successfully"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Registry ID required"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Unauthorized - missing or invalid API key"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Failed to search servers"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Search MCP servers in a registry","tags":["registries"]}},"/api/v1/secrets":{"post":{"description":"Stores a secret value in the operating system's secure keyring. The secret can then be referenced in configuration using ${keyring:secret-name} syntax. Automatically notifies runtime to restart affected servers.","requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"additionalProperties":{},"type":"object"}}},"description":"Secret stored successfully with reference syntax"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Invalid JSON payload, missing name/value, or unsupported type"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Unauthorized - missing or invalid API key"},"405":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Method not allowed"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Secret resolver not available or failed to store secret"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Store a secret in OS keyring","tags":["secrets"]}},"/api/v1/secrets/{name}":{"delete":{"description":"Deletes a secret from the operating system's secure keyring. Automatically notifies runtime to restart affected servers. Only keyring type is supported for security.","parameters":[{"description":"Name of the secret to delete","in":"path","name":"name","required":true,"schema":{"type":"string"}},{"description":"Secret type (only 'keyring' supported, defaults to 'keyring')","in":"query","name":"type","schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"additionalProperties":{},"type":"object"}}},"description":"Secret deleted successfully"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Missing secret name or unsupported type"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Unauthorized - missing or invalid API key"},"405":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Method not allowed"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Secret resolver not available or failed to delete secret"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Delete a secret from OS keyring","tags":["secrets"]}},"/api/v1/servers":{"get":{"description":"Get a list of all configured upstream MCP servers with their connection status and statistics","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.GetServersResponse"}}},"description":"Server list with statistics"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"List all upstream MCP servers","tags":["servers"]},"post":{"description":"Add a new MCP upstream server to the configuration. New servers are quarantined by default for security.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/httpapi.AddServerRequest"}}},"description":"Server configuration","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ServerActionResponse"}}},"description":"Server added successfully"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Bad request - invalid configuration"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Conflict - server with this name already exists"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Add a new upstream server","tags":["servers"]}},"/api/v1/servers/disable_all":{"post":{"description":"Disable all configured upstream MCP servers with partial failure handling","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/management.BulkOperationResult"}}},"description":"Bulk disable results with success/failure counts"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Forbidden (management disabled)"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Disable all servers","tags":["servers"]}},"/api/v1/servers/enable_all":{"post":{"description":"Enable all configured upstream MCP servers with partial failure handling","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/management.BulkOperationResult"}}},"description":"Bulk enable results with success/failure counts"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Forbidden (management disabled)"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Enable all servers","tags":["servers"]}},"/api/v1/servers/reconnect":{"post":{"description":"Force reconnection to all upstream MCP servers","parameters":[{"description":"Reason for reconnection","in":"query","name":"reason","schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ServerActionResponse"}}},"description":"All servers reconnected successfully"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Reconnect all servers","tags":["servers"]}},"/api/v1/servers/restart_all":{"post":{"description":"Restart all configured upstream MCP servers sequentially with partial failure handling","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/management.BulkOperationResult"}}},"description":"Bulk restart results with success/failure counts"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Forbidden (management disabled)"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Restart all servers","tags":["servers"]}},"/api/v1/servers/{id}":{"delete":{"description":"Remove an MCP upstream server from the configuration. This stops the server if running and removes it from config.","parameters":[{"description":"Server ID or name","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ServerActionResponse"}}},"description":"Server removed successfully"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Bad request"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Server not found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Remove an upstream server","tags":["servers"]}},"/api/v1/servers/{id}/disable":{"post":{"description":"Disable a specific upstream MCP server","parameters":[{"description":"Server ID or name","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ServerActionResponse"}}},"description":"Server disabled successfully"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Bad request"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Server not found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Disable an upstream server","tags":["servers"]}},"/api/v1/servers/{id}/discover-tools":{"post":{"description":"Manually trigger tool discovery and indexing for a specific upstream MCP server. This forces an immediate refresh of the server's tool cache.","parameters":[{"description":"Server ID or name","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ServerActionResponse"}}},"description":"Tool discovery triggered successfully"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Bad request (missing server ID)"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Server not found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Failed to discover tools"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Discover tools for a specific server","tags":["servers"]}},"/api/v1/servers/{id}/enable":{"post":{"description":"Enable a specific upstream MCP server","parameters":[{"description":"Server ID or name","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ServerActionResponse"}}},"description":"Server enabled successfully"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Bad request"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Server not found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Enable an upstream server","tags":["servers"]}},"/api/v1/servers/{id}/login":{"post":{"description":"Initiate OAuth authentication flow for a specific upstream MCP server. Returns structured OAuth start response with correlation ID for tracking.","parameters":[{"description":"Server ID or name","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.OAuthStartResponse"}}},"description":"OAuth login initiated successfully"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.OAuthFlowError"}}},"description":"OAuth error (client_id required, DCR failed, etc.)"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Server not found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Trigger OAuth login for server","tags":["servers"]}},"/api/v1/servers/{id}/logout":{"post":{"description":"Clear OAuth authentication token and disconnect a specific upstream MCP server. The server will need to re-authenticate before tools can be used again.","parameters":[{"description":"Server ID or name","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ServerActionResponse"}}},"description":"OAuth logout completed successfully"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Bad request (missing server ID)"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Forbidden (management disabled or read-only mode)"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Server not found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Clear OAuth token and disconnect server","tags":["servers"]}},"/api/v1/servers/{id}/logs":{"get":{"description":"Retrieve log entries for a specific upstream MCP server","parameters":[{"description":"Server ID or name","in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"Number of log lines to retrieve","in":"query","name":"tail","schema":{"default":100,"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.GetServerLogsResponse"}}},"description":"Server logs retrieved successfully"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Bad request (missing server ID)"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Server not found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Get server logs","tags":["servers"]}},"/api/v1/servers/{id}/quarantine":{"post":{"description":"Place a specific upstream MCP server in quarantine to prevent tool execution","parameters":[{"description":"Server ID or name","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ServerActionResponse"}}},"description":"Server quarantined successfully"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Bad request (missing server ID)"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Server not found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Quarantine a server","tags":["servers"]}},"/api/v1/servers/{id}/restart":{"post":{"description":"Restart the connection to a specific upstream MCP server","parameters":[{"description":"Server ID or name","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ServerActionResponse"}}},"description":"Server restarted successfully"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Bad request"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Server not found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Restart an upstream server","tags":["servers"]}},"/api/v1/servers/{id}/tool-calls":{"get":{"description":"Retrieves tool call history filtered by upstream server ID. Returns recent tool executions for the specified server including timestamps, arguments, results, and errors. Useful for server-specific debugging and monitoring.","parameters":[{"description":"Upstream server ID or name","in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"Maximum number of records to return (1-100, default 50)","in":"query","name":"limit","schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.GetServerToolCallsResponse"}}},"description":"Server tool calls retrieved successfully"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Server ID required"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Unauthorized - missing or invalid API key"},"405":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Method not allowed"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Failed to get server tool calls"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Get tool call history for specific server","tags":["tool-calls"]}},"/api/v1/servers/{id}/tools":{"get":{"description":"Retrieve all available tools for a specific upstream MCP server","parameters":[{"description":"Server ID or name","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.GetServerToolsResponse"}}},"description":"Server tools retrieved successfully"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Bad request (missing server ID)"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Server not found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Get tools for a server","tags":["servers"]}},"/api/v1/servers/{id}/unquarantine":{"post":{"description":"Remove a specific upstream MCP server from quarantine to allow tool execution","parameters":[{"description":"Server ID or name","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ServerActionResponse"}}},"description":"Server unquarantined successfully"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Bad request (missing server ID)"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Server not found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Unquarantine a server","tags":["servers"]}},"/api/v1/sessions":{"get":{"description":"Retrieves paginated list of active and recent MCP client sessions. Each session represents a connection from an MCP client to MCPProxy, tracking initialization time, tool calls, and connection status.","parameters":[{"description":"Maximum number of sessions to return (1-100, default 10)","in":"query","name":"limit","schema":{"type":"integer"}},{"description":"Number of sessions to skip for pagination (default 0)","in":"query","name":"offset","schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.GetSessionsResponse"}}},"description":"Sessions retrieved successfully"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Unauthorized - missing or invalid API key"},"405":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Method not allowed"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Failed to get sessions"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Get active MCP sessions","tags":["sessions"]}},"/api/v1/sessions/{id}":{"get":{"description":"Retrieves detailed information about a specific MCP client session including initialization parameters, connection status, tool call count, and activity timestamps.","parameters":[{"description":"Session ID","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.GetSessionDetailResponse"}}},"description":"Session details retrieved successfully"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Session ID required"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Unauthorized - missing or invalid API key"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Session not found"},"405":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Method not allowed"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Get MCP session details by ID","tags":["sessions"]}},"/api/v1/stats/tokens":{"get":{"description":"Retrieve token savings statistics across all servers and sessions","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.SuccessResponse"}}},"description":"Token statistics"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Get token savings statistics","tags":["stats"]}},"/api/v1/status":{"get":{"description":"Get comprehensive server status including running state, listen address, upstream statistics, and timestamp","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.SuccessResponse"}}},"description":"Server status information"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Get server status","tags":["status"]}},"/api/v1/tool-calls":{"get":{"description":"Retrieves paginated tool call history across all upstream servers or filtered by session ID. Includes execution timestamps, arguments, results, and error information for debugging and auditing.","parameters":[{"description":"Maximum number of records to return (1-100, default 50)","in":"query","name":"limit","schema":{"type":"integer"}},{"description":"Number of records to skip for pagination (default 0)","in":"query","name":"offset","schema":{"type":"integer"}},{"description":"Filter tool calls by MCP session ID","in":"query","name":"session_id","schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.GetToolCallsResponse"}}},"description":"Tool calls retrieved successfully"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Unauthorized - missing or invalid API key"},"405":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Method not allowed"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Failed to get tool calls"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Get tool call history","tags":["tool-calls"]}},"/api/v1/tool-calls/{id}":{"get":{"description":"Retrieves detailed information about a specific tool call execution including full request arguments, response data, execution time, and any errors encountered.","parameters":[{"description":"Tool call ID","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.GetToolCallDetailResponse"}}},"description":"Tool call details retrieved successfully"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Tool call ID required"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Unauthorized - missing or invalid API key"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Tool call not found"},"405":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Method not allowed"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Get tool call details by ID","tags":["tool-calls"]}},"/api/v1/tool-calls/{id}/replay":{"post":{"description":"Re-executes a previous tool call with optional modified arguments. Useful for debugging and testing tool behavior with different inputs. Creates a new tool call record linked to the original.","parameters":[{"description":"Original tool call ID to replay","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ReplayToolCallRequest"}}},"description":"Optional modified arguments for replay"},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ReplayToolCallResponse"}}},"description":"Tool call replayed successfully"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Tool call ID required or invalid JSON payload"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Unauthorized - missing or invalid API key"},"405":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Method not allowed"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Failed to replay tool call"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Replay a tool call","tags":["tool-calls"]}},"/api/v1/tools/call":{"post":{"description":"Execute a tool on an upstream MCP server (wrapper around MCP tool calls)","requestBody":{"content":{"application/json":{"schema":{"properties":{"arguments":{"type":"object"},"tool_name":{"type":"string"}},"type":"object"}}},"description":"Tool call request with tool name and arguments","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.SuccessResponse"}}},"description":"Tool call result"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Bad request (invalid payload or missing tool name)"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/contracts.ErrorResponse"}}},"description":"Internal server error or tool execution failure"}},"security":[{"ApiKeyAuth":[]},{"ApiKeyQuery":[]}],"summary":"Call a tool","tags":["tools"]}},"/healthz":{"get":{"description":"Get comprehensive health status including all component health (Kubernetes-compatible liveness probe)","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/observability.HealthResponse"}}},"description":"Service is healthy"},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/observability.HealthResponse"}}},"description":"Service is unhealthy"}},"summary":"Get health status","tags":["health"]}},"/readyz":{"get":{"description":"Get readiness status including all component readiness checks (Kubernetes-compatible readiness probe)","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/observability.ReadinessResponse"}}},"description":"Service is ready"},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/observability.ReadinessResponse"}}},"description":"Service is not ready"}},"summary":"Get readiness status","tags":["health"]}}},
"openapi": "3.1.0"
}`
diff --git a/oas/swagger.yaml b/oas/swagger.yaml
index d30032b7..6cb0a21d 100644
--- a/oas/swagger.yaml
+++ b/oas/swagger.yaml
@@ -1549,7 +1549,8 @@ paths:
get:
description: Returns paginated list of activity records with optional filtering
parameters:
- - description: Filter by activity type
+ - description: Filter by activity type(s), comma-separated for multiple (Spec
+ 024)
in: query
name: type
schema:
@@ -1558,6 +1559,10 @@ paths:
- policy_decision
- quarantine_change
- server_change
+ - system_start
+ - system_stop
+ - internal_tool_call
+ - config_change
type: string
- description: Filter by server name
in: query
@@ -1597,6 +1602,12 @@ paths:
name: request_id
schema:
type: string
+ - description: 'Include successful call_tool_* internal tool calls (default:
+ false, excluded to avoid duplicates)'
+ in: query
+ name: include_call_tool
+ schema:
+ type: boolean
- description: Filter activities after this time (RFC3339)
in: query
name: start_time
diff --git a/specs/024-expand-activity-log/checklists/requirements.md b/specs/024-expand-activity-log/checklists/requirements.md
new file mode 100644
index 00000000..a17cffe7
--- /dev/null
+++ b/specs/024-expand-activity-log/checklists/requirements.md
@@ -0,0 +1,45 @@
+# Specification Quality Checklist: Expand Activity Log
+
+**Purpose**: Validate specification completeness and quality before proceeding to planning
+**Created**: 2026-01-12
+**Feature**: [spec.md](../spec.md)
+
+## Content Quality
+
+- [x] No implementation details (languages, frameworks, APIs)
+- [x] Focused on user value and business needs
+- [x] Written for non-technical stakeholders
+- [x] All mandatory sections completed
+
+## Requirement Completeness
+
+- [x] No [NEEDS CLARIFICATION] markers remain
+- [x] Requirements are testable and unambiguous
+- [x] Success criteria are measurable
+- [x] Success criteria are technology-agnostic (no implementation details)
+- [x] All acceptance scenarios are defined
+- [x] Edge cases are identified
+- [x] Scope is clearly bounded
+- [x] Dependencies and assumptions identified
+
+## Feature Readiness
+
+- [x] All functional requirements have clear acceptance criteria
+- [x] User scenarios cover primary flows
+- [x] Feature meets measurable outcomes defined in Success Criteria
+- [x] No implementation details leak into specification
+- [x] Testing approach defined (Web UI via Claude extension, CLI via shell)
+- [x] Documentation requirements included (Docusaurus updates)
+
+## Notes
+
+- All items pass validation
+- Spec is ready for `/speckit.clarify` or `/speckit.plan`
+- The existing JsonViewer component already supports syntax highlighting based on review of current Activity.vue implementation
+- Activity Log menu item already exists but is commented out in SidebarNav.vue - spec documents enabling it
+- CLI already supports `--type` flag but only for single type - spec extends to comma-separated multiple types
+- Documentation updates required in `docs/` Docusaurus site
+- Testing approach includes:
+ - Web UI testing via Claude browser automation (mcp__claude-in-chrome__* or mcp__playwriter__*)
+ - CLI testing via Bash tool
+ - Backend testing via API calls
diff --git a/specs/024-expand-activity-log/contracts/activity-api-changes.yaml b/specs/024-expand-activity-log/contracts/activity-api-changes.yaml
new file mode 100644
index 00000000..f4d656e6
--- /dev/null
+++ b/specs/024-expand-activity-log/contracts/activity-api-changes.yaml
@@ -0,0 +1,312 @@
+# Activity API Changes for Spec 024
+# These changes extend the existing activity API endpoints
+
+openapi: 3.0.0
+info:
+ title: Activity API Extensions (Spec 024)
+ version: 1.0.0
+ description: |
+ Extensions to the Activity Log API for Spec 024 (Expand Activity Log).
+
+ Changes:
+ - Multi-type filter support (comma-separated types)
+ - New event types: system_start, system_stop, internal_tool_call, config_change
+
+paths:
+ /api/v1/activity:
+ get:
+ summary: List activity records
+ description: |
+ List activity records with filtering and pagination.
+
+ **New in Spec 024**: The `type` parameter now accepts comma-separated values
+ for filtering by multiple event types (OR logic).
+ parameters:
+ - name: type
+ in: query
+ description: |
+ Filter by activity type(s). Accepts comma-separated values for multi-type filtering.
+
+ Valid types:
+ - `tool_call` - Upstream tool executions
+ - `policy_decision` - Policy blocking tool calls
+ - `quarantine_change` - Server quarantine changes
+ - `server_change` - Server enable/disable/restart
+ - `system_start` - MCPProxy server started (NEW)
+ - `system_stop` - MCPProxy server stopped (NEW)
+ - `internal_tool_call` - Internal MCP tool calls (NEW)
+ - `config_change` - Configuration changes (NEW)
+
+ Example: `?type=tool_call,internal_tool_call`
+ schema:
+ type: string
+ example: "tool_call,internal_tool_call"
+ - name: server
+ in: query
+ description: Filter by server name
+ schema:
+ type: string
+ - name: status
+ in: query
+ description: Filter by status (success, error, blocked)
+ schema:
+ type: string
+ enum: [success, error, blocked]
+ - name: intent_type
+ in: query
+ description: Filter by intent operation type
+ schema:
+ type: string
+ enum: [read, write, destructive]
+ - name: request_id
+ in: query
+ description: Filter by HTTP request ID
+ schema:
+ type: string
+ - name: limit
+ in: query
+ description: Maximum records to return (1-100, default 50)
+ schema:
+ type: integer
+ minimum: 1
+ maximum: 100
+ default: 50
+ - name: offset
+ in: query
+ description: Pagination offset
+ schema:
+ type: integer
+ default: 0
+ responses:
+ '200':
+ description: Activity records
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ActivityListResponse'
+
+components:
+ schemas:
+ ActivityType:
+ type: string
+ description: Type of activity event
+ enum:
+ - tool_call
+ - policy_decision
+ - quarantine_change
+ - server_change
+ - system_start # NEW
+ - system_stop # NEW
+ - internal_tool_call # NEW
+ - config_change # NEW
+
+ ActivityRecord:
+ type: object
+ required:
+ - id
+ - type
+ - status
+ - timestamp
+ properties:
+ id:
+ type: string
+ description: Unique identifier (ULID format)
+ example: "01JFXYZ123ABC"
+ type:
+ $ref: '#/components/schemas/ActivityType'
+ source:
+ type: string
+ description: How activity was triggered
+ enum: [mcp, cli, api]
+ server_name:
+ type: string
+ description: Name of upstream MCP server (if applicable)
+ tool_name:
+ type: string
+ description: Name of tool called (if applicable)
+ arguments:
+ type: object
+ description: Tool call arguments
+ additionalProperties: true
+ response:
+ type: string
+ description: Tool response (potentially truncated)
+ response_truncated:
+ type: boolean
+ description: True if response was truncated
+ status:
+ type: string
+ description: Result status
+ enum: [success, error, blocked]
+ error_message:
+ type: string
+ description: Error details if status is error
+ duration_ms:
+ type: integer
+ description: Execution duration in milliseconds
+ timestamp:
+ type: string
+ format: date-time
+ description: When activity occurred
+ session_id:
+ type: string
+ description: MCP session ID for correlation
+ request_id:
+ type: string
+ description: HTTP request ID for correlation
+ metadata:
+ type: object
+ description: Type-specific metadata
+ additionalProperties: true
+
+ # NEW: System Start Metadata
+ SystemStartMetadata:
+ type: object
+ description: Metadata for system_start events
+ properties:
+ version:
+ type: string
+ description: MCPProxy version
+ example: "v1.2.3"
+ listen_address:
+ type: string
+ description: HTTP listener address
+ example: "127.0.0.1:8080"
+ startup_duration_ms:
+ type: integer
+ description: Time from command start to ready
+ example: 1250
+ config_path:
+ type: string
+ description: Path to config file used
+ example: "/Users/user/.mcpproxy/mcp_config.json"
+ unclean_previous_shutdown:
+ type: boolean
+ description: True if previous session didn't log system_stop
+
+ # NEW: System Stop Metadata
+ SystemStopMetadata:
+ type: object
+ description: Metadata for system_stop events
+ properties:
+ reason:
+ type: string
+ description: Shutdown reason
+ enum: [signal, manual, error]
+ signal:
+ type: string
+ description: Signal name if reason is signal
+ example: "SIGTERM"
+ uptime_seconds:
+ type: integer
+ description: Total server uptime
+ example: 3600
+ error_message:
+ type: string
+ description: Error details if reason is error
+
+ # NEW: Internal Tool Call Metadata
+ InternalToolCallMetadata:
+ type: object
+ description: Metadata for internal_tool_call events
+ properties:
+ internal_tool_name:
+ type: string
+ description: Name of internal tool called
+ enum:
+ - retrieve_tools
+ - call_tool_read
+ - call_tool_write
+ - call_tool_destructive
+ - code_execution
+ - upstream_servers
+ - quarantine_security
+ - list_registries
+ - search_servers
+ - read_cache
+ target_server:
+ type: string
+ description: Target upstream server (for call_tool_*)
+ target_tool:
+ type: string
+ description: Target upstream tool (for call_tool_*)
+ tool_variant:
+ type: string
+ description: Tool variant used
+ enum: [call_tool_read, call_tool_write, call_tool_destructive]
+ intent:
+ $ref: '#/components/schemas/IntentDeclaration'
+
+ # NEW: Config Change Metadata
+ ConfigChangeMetadata:
+ type: object
+ description: Metadata for config_change events
+ properties:
+ action:
+ type: string
+ description: Type of config change
+ enum:
+ - server_added
+ - server_removed
+ - server_updated
+ - settings_changed
+ affected_entity:
+ type: string
+ description: Name of affected server or "global"
+ changed_fields:
+ type: array
+ items:
+ type: string
+ description: List of fields that changed
+ previous_values:
+ type: object
+ description: Previous values for changed fields
+ additionalProperties: true
+ new_values:
+ type: object
+ description: New values for changed fields
+ additionalProperties: true
+
+ IntentDeclaration:
+ type: object
+ description: Intent declaration from Spec 018
+ properties:
+ operation_type:
+ type: string
+ enum: [read, write, destructive]
+ data_sensitivity:
+ type: string
+ enum: [public, internal, private, unknown]
+ reason:
+ type: string
+ description: Agent's explanation for the operation
+
+ ActivityListResponse:
+ type: object
+ required:
+ - success
+ - data
+ properties:
+ success:
+ type: boolean
+ data:
+ type: object
+ required:
+ - activities
+ - total
+ - limit
+ - offset
+ properties:
+ activities:
+ type: array
+ items:
+ $ref: '#/components/schemas/ActivityRecord'
+ total:
+ type: integer
+ description: Total matching records
+ limit:
+ type: integer
+ description: Records per page
+ offset:
+ type: integer
+ description: Current offset
diff --git a/specs/024-expand-activity-log/data-model.md b/specs/024-expand-activity-log/data-model.md
new file mode 100644
index 00000000..607edf2c
--- /dev/null
+++ b/specs/024-expand-activity-log/data-model.md
@@ -0,0 +1,238 @@
+# Data Model: Expand Activity Log
+
+**Date**: 2026-01-12
+**Feature**: 024-expand-activity-log
+
+## Entity Changes
+
+### 1. ActivityType Enumeration (Extended)
+
+**Location**: `internal/storage/activity_models.go`
+
+| Constant | Value | Description |
+|----------|-------|-------------|
+| `ActivityTypeToolCall` | `"tool_call"` | Upstream tool execution (existing) |
+| `ActivityTypePolicyDecision` | `"policy_decision"` | Policy blocking a tool call (existing) |
+| `ActivityTypeQuarantineChange` | `"quarantine_change"` | Server quarantine state change (existing) |
+| `ActivityTypeServerChange` | `"server_change"` | Server enable/disable/restart (existing) |
+| `ActivityTypeSystemStart` | `"system_start"` | **NEW**: MCPProxy server started |
+| `ActivityTypeSystemStop` | `"system_stop"` | **NEW**: MCPProxy server stopped |
+| `ActivityTypeInternalToolCall` | `"internal_tool_call"` | **NEW**: Internal MCP tool call |
+| `ActivityTypeConfigChange` | `"config_change"` | **NEW**: Configuration change |
+
+### 2. ActivityRecord (Extended Metadata)
+
+**Location**: `internal/storage/activity_models.go`
+
+The `ActivityRecord` struct remains unchanged. New event types use the existing `Metadata` field to store type-specific data.
+
+```go
+type ActivityRecord struct {
+ ID string `json:"id"`
+ Type ActivityType `json:"type"`
+ Source ActivitySource `json:"source,omitempty"`
+ ServerName string `json:"server_name,omitempty"`
+ ToolName string `json:"tool_name,omitempty"`
+ Arguments map[string]interface{} `json:"arguments,omitempty"`
+ Response string `json:"response,omitempty"`
+ ResponseTruncated bool `json:"response_truncated,omitempty"`
+ Status string `json:"status"`
+ ErrorMessage string `json:"error_message,omitempty"`
+ DurationMs int64 `json:"duration_ms,omitempty"`
+ Timestamp time.Time `json:"timestamp"`
+ SessionID string `json:"session_id,omitempty"`
+ RequestID string `json:"request_id,omitempty"`
+ Metadata map[string]interface{} `json:"metadata,omitempty"`
+}
+```
+
+### 3. Type-Specific Metadata Schemas
+
+#### 3.1 SystemStartMetadata
+
+For `system_start` events:
+
+```json
+{
+ "version": "v1.2.3",
+ "listen_address": "127.0.0.1:8080",
+ "startup_duration_ms": 1250,
+ "config_path": "/Users/user/.mcpproxy/mcp_config.json",
+ "unclean_previous_shutdown": false
+}
+```
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `version` | string | MCPProxy version |
+| `listen_address` | string | HTTP listener address |
+| `startup_duration_ms` | int64 | Time from command start to ready |
+| `config_path` | string | Path to config file used |
+| `unclean_previous_shutdown` | bool | True if previous session didn't log system_stop |
+
+#### 3.2 SystemStopMetadata
+
+For `system_stop` events:
+
+```json
+{
+ "reason": "signal",
+ "signal": "SIGTERM",
+ "uptime_seconds": 3600,
+ "error_message": ""
+}
+```
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `reason` | string | Shutdown reason: `"signal"`, `"manual"`, `"error"` |
+| `signal` | string | Signal name if reason is signal |
+| `uptime_seconds` | int64 | Total server uptime |
+| `error_message` | string | Error details if reason is error |
+
+#### 3.3 InternalToolCallMetadata
+
+For `internal_tool_call` events:
+
+```json
+{
+ "internal_tool_name": "retrieve_tools",
+ "target_server": "github",
+ "target_tool": "create_issue",
+ "tool_variant": "call_tool_write",
+ "intent": {
+ "operation_type": "write",
+ "data_sensitivity": "internal",
+ "reason": "Creating issue per user request"
+ }
+}
+```
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `internal_tool_name` | string | Name of internal tool called |
+| `target_server` | string | Target upstream server (for call_tool_*) |
+| `target_tool` | string | Target upstream tool (for call_tool_*) |
+| `tool_variant` | string | Tool variant used (call_tool_read/write/destructive) |
+| `intent` | object | Intent declaration (from Spec 018) |
+
+#### 3.4 ConfigChangeMetadata
+
+For `config_change` events:
+
+```json
+{
+ "action": "server_added",
+ "affected_entity": "github-server",
+ "changed_fields": ["enabled", "quarantined"],
+ "previous_values": {"enabled": false},
+ "new_values": {"enabled": true}
+}
+```
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `action` | string | `"server_added"`, `"server_removed"`, `"server_updated"`, `"settings_changed"` |
+| `affected_entity` | string | Name of affected server or "global" |
+| `changed_fields` | []string | List of fields that changed |
+| `previous_values` | object | Previous values for changed fields |
+| `new_values` | object | New values for changed fields |
+
+### 4. ActivityFilter (Extended)
+
+**Location**: `internal/storage/activity_models.go`
+
+```go
+type ActivityFilter struct {
+ Types []string // Changed from single Type string - supports multi-type OR filter
+ Server string
+ Tool string
+ SessionID string
+ Status string
+ StartTime time.Time
+ EndTime time.Time
+ Limit int
+ Offset int
+ IntentType string
+ RequestID string
+}
+```
+
+**Changes**:
+- `Type string` → `Types []string`
+- `Matches()` method updated for OR logic on types
+
+### 5. EventType Constants (Extended)
+
+**Location**: `internal/runtime/events.go`
+
+| Constant | Value | Description |
+|----------|-------|-------------|
+| `EventTypeActivitySystemStart` | `"activity.system.start"` | **NEW**: System started |
+| `EventTypeActivitySystemStop` | `"activity.system.stop"` | **NEW**: System stopped |
+| `EventTypeActivityInternalToolCall` | `"activity.internal_tool_call.completed"` | **NEW**: Internal tool call completed |
+| `EventTypeActivityConfigChange` | `"activity.config_change"` | **NEW**: Config changed |
+
+## Validation Rules
+
+### ActivityType Validation
+
+Valid activity types for filtering:
+
+```go
+var ValidActivityTypes = []string{
+ "tool_call",
+ "policy_decision",
+ "quarantine_change",
+ "server_change",
+ "system_start",
+ "system_stop",
+ "internal_tool_call",
+ "config_change",
+}
+```
+
+### ConfigChange Action Validation
+
+Valid config change actions:
+
+```go
+var ValidConfigActions = []string{
+ "server_added",
+ "server_removed",
+ "server_updated",
+ "settings_changed",
+}
+```
+
+## State Transitions
+
+### Server Lifecycle
+
+```
+[Not Running] → system_start → [Running] → system_stop → [Not Running]
+```
+
+### Config Change Flow
+
+```
+[User Action] → [API/CLI Handler] → [ServerManager] → [Storage]
+ ↓
+ [EventBus: servers.changed]
+ ↓
+ [ActivityService] → config_change record
+```
+
+## Storage Considerations
+
+- **No schema migration required**: New activity types use existing BBolt bucket
+- **Metadata flexibility**: Type-specific data stored in `Metadata` map
+- **Retention unchanged**: Existing retention policy applies to all activity types
+- **Index unchanged**: No new BBolt indexes needed (filter by type uses iteration)
+
+## Backwards Compatibility
+
+- Existing `Type` filter API parameter continues to work (treated as single-element array)
+- Existing activity records remain valid
+- Clients that don't understand new types will see them as unknown strings
+- SSE events for new types will be ignored by old clients
diff --git a/specs/024-expand-activity-log/plan.md b/specs/024-expand-activity-log/plan.md
new file mode 100644
index 00000000..0c9b7520
--- /dev/null
+++ b/specs/024-expand-activity-log/plan.md
@@ -0,0 +1,147 @@
+# Implementation Plan: Expand Activity Log
+
+**Branch**: `024-expand-activity-log` | **Date**: 2026-01-12 | **Spec**: [spec.md](./spec.md)
+**Input**: Feature specification from `/specs/024-expand-activity-log/spec.md`
+
+## Summary
+
+Expand the Activity Log to capture system lifecycle events (start/stop), internal tool calls (retrieve_tools, call_tool_*, code_execution), and configuration changes. Enhance the Web UI with multi-select event type filtering, an Intent column, and sortable columns. Extend CLI to support comma-separated event type filtering. Update Docusaurus documentation.
+
+## Technical Context
+
+**Language/Version**: Go 1.24 (toolchain go1.24.10) + TypeScript 5.x / Vue 3.5
+**Primary Dependencies**: Cobra CLI, Chi router, BBolt storage, Zap logging, mark3labs/mcp-go, Vue 3, Tailwind CSS, DaisyUI
+**Storage**: BBolt database (`~/.mcpproxy/config.db`) - ActivityRecord model
+**Testing**: `go test`, E2E via `./scripts/test-api-e2e.sh`, Claude browser automation for Web UI
+**Target Platform**: macOS, Linux, Windows desktop
+**Project Type**: Web application (Go backend + Vue frontend)
+**Performance Goals**: Activity records queryable within 100ms for up to 10,000 records; SSE events delivered within 50ms
+**Constraints**: Non-blocking activity recording; existing retention policy (7 days, 10,000 records)
+**Scale/Scope**: Up to 1,000 tools across multiple upstream servers; Activity Log handles ~10,000 records
+
+## Constitution Check
+
+*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
+
+| Principle | Status | Notes |
+|-----------|--------|-------|
+| I. Performance at Scale | ✅ PASS | Activity recording is non-blocking via event-driven architecture. No changes to tool indexing/search. |
+| II. Actor-Based Concurrency | ✅ PASS | ActivityService uses goroutine + channel pattern. New event types use same pattern. |
+| III. Configuration-Driven Architecture | ✅ PASS | No new config settings required. Uses existing activity retention config. |
+| IV. Security by Default | ✅ PASS | Activity logging enhances auditability. No security regressions. |
+| V. Test-Driven Development (TDD) | ✅ PASS | Tests required for new event types, API changes, and Web UI features. |
+| VI. Documentation Hygiene | ✅ PASS | Documentation updates explicitly required in spec (FR-027 to FR-030). |
+
+**Architecture Constraints:**
+
+| Constraint | Status | Notes |
+|------------|--------|-------|
+| Core + Tray Split | ✅ PASS | Changes are in core only. Tray receives updates via SSE. |
+| Event-Driven Updates | ✅ PASS | New event types use existing EventBus pattern. |
+| DDD Layering | ✅ PASS | Storage models in `internal/storage/`, service in `internal/runtime/`, API in `internal/httpapi/`. |
+| Upstream Client Modularity | ✅ N/A | No changes to upstream client layers. |
+
+## Project Structure
+
+### Documentation (this feature)
+
+```text
+specs/024-expand-activity-log/
+├── plan.md # This file
+├── research.md # Phase 0 output
+├── data-model.md # Phase 1 output
+├── quickstart.md # Phase 1 output
+├── contracts/ # Phase 1 output
+└── tasks.md # Phase 2 output (via /speckit.tasks)
+```
+
+### Source Code (repository root)
+
+```text
+# Backend (Go)
+internal/
+├── storage/
+│ └── activity_models.go # Extended ActivityType enum
+├── runtime/
+│ ├── activity_service.go # Extended handlers for new event types
+│ └── events.go # New EventType constants
+├── httpapi/
+│ └── activity.go # Multi-type filter support in API
+└── server/
+ └── mcp_handler.go # Emit internal_tool_call events
+
+cmd/mcpproxy/
+├── activity_cmd.go # Extended --type flag validation
+└── serve_cmd.go # Emit system_start/system_stop events
+
+# Frontend (Vue)
+frontend/src/
+├── views/
+│ └── Activity.vue # Multi-select filter, Intent column, sortable columns
+├── components/
+│ ├── SidebarNav.vue # Enable Activity Log menu item
+│ └── JsonViewer.vue # Already exists with syntax highlighting
+└── types/
+ └── api.ts # Extended ActivityType enum
+
+# Documentation (Docusaurus)
+docs/
+├── features/
+│ └── activity-log.md # Document new event types
+└── cli/
+ └── activity-commands.md # Document multi-type filtering
+
+# Tests
+internal/
+├── storage/activity_test.go # Tests for new activity types
+├── runtime/activity_service_test.go # Tests for event handlers
+└── httpapi/activity_test.go # Tests for multi-type filter API
+```
+
+**Structure Decision**: Existing web application structure with Go backend and Vue frontend. Changes extend existing files rather than creating new ones.
+
+## Complexity Tracking
+
+No constitution violations requiring justification. The implementation extends existing patterns:
+
+- New ActivityType constants extend existing enum
+- New event handlers follow existing ActivityService pattern
+- Multi-type filtering extends existing filter logic
+- Web UI changes extend existing Activity.vue component
+
+## Constitution Check (Post-Design)
+
+*Re-evaluation after Phase 1 design artifacts completed.*
+
+| Principle | Status | Post-Design Notes |
+|-----------|--------|-------------------|
+| I. Performance at Scale | ✅ PASS | Multi-type filter uses OR logic on iteration; pagination limits to 100 records so performance impact minimal. Client-side sorting handles max 100 records. |
+| II. Actor-Based Concurrency | ✅ PASS | All new event emissions go through existing EventBus channel. ActivityService handles events in single goroutine. |
+| III. Configuration-Driven Architecture | ✅ PASS | No new config required. All new features use existing infrastructure. |
+| IV. Security by Default | ✅ PASS | Config change logging enhances audit trail. Internal tool call logging provides visibility into agent behavior. |
+| V. Test-Driven Development (TDD) | ✅ PASS | quickstart.md defines test plan. Unit tests for new types, E2E for API, manual UI testing via browser automation. |
+| VI. Documentation Hygiene | ✅ PASS | Documentation files identified in quickstart.md. Docusaurus updates planned for features and CLI sections. |
+
+**Architecture Constraints (Post-Design):**
+
+| Constraint | Status | Post-Design Notes |
+|------------|--------|-------------------|
+| Core + Tray Split | ✅ PASS | All changes in core server. SSE delivers events to tray/web UI. No tray code changes needed. |
+| Event-Driven Updates | ✅ PASS | Four new EventType constants. ActivityService extended to handle config_change events. |
+| DDD Layering | ✅ PASS | data-model.md confirms: storage models, runtime service, httpapi handler, server emission points. |
+| Upstream Client Modularity | ✅ N/A | No changes to upstream client layers. |
+
+## Phase 1 Artifacts
+
+| Artifact | Path | Status |
+|----------|------|--------|
+| Research | [research.md](./research.md) | Complete |
+| Data Model | [data-model.md](./data-model.md) | Complete |
+| API Contracts | [contracts/activity-api-changes.yaml](./contracts/activity-api-changes.yaml) | Complete |
+| Quickstart | [quickstart.md](./quickstart.md) | Complete |
+
+## Next Steps
+
+1. Run `/speckit.tasks` to generate tasks.md
+2. Implement in order defined in quickstart.md
+3. Test using Claude browser automation for Web UI and Bash for CLI
diff --git a/specs/024-expand-activity-log/quickstart.md b/specs/024-expand-activity-log/quickstart.md
new file mode 100644
index 00000000..3f192a7f
--- /dev/null
+++ b/specs/024-expand-activity-log/quickstart.md
@@ -0,0 +1,259 @@
+# Quickstart: Expand Activity Log (Spec 024)
+
+This guide provides the implementation order and key files for each component.
+
+## Implementation Order
+
+1. **Backend: New Event Types** (P1)
+2. **Backend: Multi-Type Filter** (P2)
+3. **Web UI: Enable Activity Log Menu** (P2)
+4. **Web UI: Multi-Select Filter** (P2)
+5. **Web UI: Intent Column & Sorting** (P2)
+6. **CLI: Multi-Type Filter** (P3)
+7. **Documentation** (P3)
+8. **Testing & Validation**
+
+---
+
+## 1. Backend: New Event Types
+
+### 1.1 Add ActivityType Constants
+
+**File**: `internal/storage/activity_models.go`
+
+```go
+const (
+ // Existing types...
+ ActivityTypeToolCall ActivityType = "tool_call"
+ ActivityTypePolicyDecision ActivityType = "policy_decision"
+ ActivityTypeQuarantineChange ActivityType = "quarantine_change"
+ ActivityTypeServerChange ActivityType = "server_change"
+
+ // NEW: Spec 024
+ ActivityTypeSystemStart ActivityType = "system_start"
+ ActivityTypeSystemStop ActivityType = "system_stop"
+ ActivityTypeInternalToolCall ActivityType = "internal_tool_call"
+ ActivityTypeConfigChange ActivityType = "config_change"
+)
+```
+
+### 1.2 Add EventType Constants
+
+**File**: `internal/runtime/events.go`
+
+```go
+const (
+ // Existing...
+
+ // NEW: Spec 024
+ EventTypeActivitySystemStart EventType = "activity.system.start"
+ EventTypeActivitySystemStop EventType = "activity.system.stop"
+ EventTypeActivityInternalToolCall EventType = "activity.internal_tool_call.completed"
+ EventTypeActivityConfigChange EventType = "activity.config_change"
+)
+```
+
+### 1.3 Emit System Start/Stop Events
+
+**File**: `internal/server/server.go`
+
+In `StartServer()` method after HTTP listener bind:
+```go
+s.runtime.EmitActivitySystemStart(version, listenAddr, startupDurationMs, configPath)
+```
+
+In `Shutdown()` method at the beginning:
+```go
+s.runtime.EmitActivitySystemStop(reason, uptimeSeconds, errorMsg)
+```
+
+### 1.4 Emit Internal Tool Call Events
+
+**File**: `internal/server/mcp.go`
+
+In each internal tool handler (`handleRetrieveTools`, `handleCallToolRead`, etc.):
+```go
+p.emitActivityInternalToolCall(toolName, targetServer, targetTool, args, status, durationMs, intent)
+```
+
+### 1.5 Emit Config Change Events
+
+**File**: `internal/runtime/activity_service.go`
+
+Subscribe to `EventTypeServersChanged` and create `config_change` activity records.
+
+---
+
+## 2. Backend: Multi-Type Filter
+
+### 2.1 Update ActivityFilter
+
+**File**: `internal/storage/activity_models.go`
+
+```go
+type ActivityFilter struct {
+ Types []string // Changed from Type string
+ Server string
+ // ... rest unchanged
+}
+```
+
+Update `Matches()` method for OR logic.
+
+### 2.2 Update API Handler
+
+**File**: `internal/httpapi/activity.go`
+
+Parse comma-separated `type` parameter:
+```go
+typeParam := r.URL.Query().Get("type")
+if typeParam != "" {
+ filter.Types = strings.Split(typeParam, ",")
+}
+```
+
+---
+
+## 3. Web UI: Enable Activity Log Menu
+
+**File**: `frontend/src/components/SidebarNav.vue`
+
+Uncomment or add Activity Log menu item:
+```vue
+
+ Activity Log
+
+```
+
+---
+
+## 4. Web UI: Multi-Select Filter
+
+**File**: `frontend/src/views/Activity.vue`
+
+Create multi-select dropdown component:
+```vue
+
+
+
+
+```
+
+---
+
+## 5. Web UI: Intent Column & Sorting
+
+**File**: `frontend/src/views/Activity.vue`
+
+### Intent Column
+
+Add column to table:
+```vue
+ | Intent |
+
+
+
+ {{ getIntentIcon(activity.metadata.intent.operation_type) }}
+ {{ truncate(activity.metadata.intent.reason, 30) }}
+
+ -
+ |
+```
+
+### Sortable Columns
+
+Add sort state and click handlers:
+```vue
+
+ Time {{ getSortIndicator('timestamp') }}
+ |
+```
+
+---
+
+## 6. CLI: Multi-Type Filter
+
+**File**: `cmd/mcpproxy/activity_cmd.go`
+
+Update type flag handling:
+```go
+typeFlag, _ := cmd.Flags().GetString("type")
+if typeFlag != "" {
+ types := strings.Split(typeFlag, ",")
+ for _, t := range types {
+ if !isValidActivityType(t) {
+ return fmt.Errorf("invalid type: %s. Valid types: %s", t, validTypesString)
+ }
+ }
+ params.Set("type", typeFlag)
+}
+```
+
+---
+
+## 7. Documentation
+
+### Files to Update
+
+1. **`docs/features/activity-log.md`**
+ - Add new event types to table
+ - Add examples for system_start, system_stop, internal_tool_call, config_change
+
+2. **`docs/cli/activity-commands.md`**
+ - Add multi-type filter examples
+ - Document valid event types
+
+3. **`docs/web-ui/activity-log.md`** (create if needed)
+ - Document multi-select filter
+ - Document Intent column
+ - Document sortable columns
+
+---
+
+## 8. Testing & Validation
+
+### Unit Tests
+
+```bash
+go test ./internal/storage/... -v -run TestActivity
+go test ./internal/runtime/... -v -run TestActivity
+go test ./internal/httpapi/... -v -run TestActivity
+```
+
+### E2E Tests
+
+```bash
+./scripts/test-api-e2e.sh
+```
+
+### Manual Testing
+
+1. **System Events**: Start/stop MCPProxy and check activity log
+2. **Internal Tools**: Call retrieve_tools via MCP client
+3. **Config Changes**: Add/remove server via CLI
+4. **Multi-Type Filter**: Test API with `?type=tool_call,config_change`
+5. **Web UI**: Verify multi-select, Intent column, sorting
+
+---
+
+## Key Files Summary
+
+| Component | Files |
+|-----------|-------|
+| Activity Types | `internal/storage/activity_models.go` |
+| Event Types | `internal/runtime/events.go` |
+| Event Emission | `internal/server/server.go`, `internal/server/mcp.go` |
+| Event Handling | `internal/runtime/activity_service.go` |
+| REST API | `internal/httpapi/activity.go` |
+| CLI | `cmd/mcpproxy/activity_cmd.go` |
+| Web UI | `frontend/src/views/Activity.vue`, `frontend/src/components/SidebarNav.vue` |
+| Docs | `docs/features/activity-log.md`, `docs/cli/activity-commands.md` |
diff --git a/specs/024-expand-activity-log/research.md b/specs/024-expand-activity-log/research.md
new file mode 100644
index 00000000..ed8ea2fb
--- /dev/null
+++ b/specs/024-expand-activity-log/research.md
@@ -0,0 +1,225 @@
+# Research: Expand Activity Log
+
+**Date**: 2026-01-12
+**Feature**: 024-expand-activity-log
+
+## 1. New Event Types Implementation
+
+### 1.1 System Start/Stop Events
+
+**Research Task**: Where to emit system_start and system_stop events?
+
+**Findings**:
+- Server lifecycle is in `cmd/mcpproxy/main.go`:
+ - `runServer()` function handles server startup (line 371)
+ - Server start: After `srv.StartServer(ctx)` succeeds (line 588)
+ - Server stop: Before `srv.Shutdown()` is called (line 597)
+- Signal handling is in a goroutine (lines 560-584)
+- Runtime is available via `srv.runtime` after `server.NewServerWithConfigPath()`
+
+**Decision**: Emit events from the Server struct in `internal/server/server.go`:
+- `system_start` after successful HTTP listener bind and before returning from `StartServer()`
+- `system_stop` at the beginning of `Shutdown()` method
+
+**Rationale**: This keeps event emission close to the actual lifecycle transitions and ensures runtime is available.
+
+**Alternatives Considered**:
+1. Emit from `cmd/mcpproxy/main.go` - Rejected: Runtime not directly accessible
+2. Emit from runtime initialization - Rejected: Too early, server not ready
+
+### 1.2 Internal Tool Call Events
+
+**Research Task**: How to capture internal tool calls (retrieve_tools, call_tool_*, etc.)?
+
+**Findings**:
+- Internal tools are registered in `internal/server/mcp.go` `registerTools()` method (line 266)
+- Tool handlers: `handleRetrieveTools`, `handleCallToolRead`, `handleCallToolWrite`, `handleCallToolDestructive`
+- Existing activity emission pattern uses `emitActivityToolCallCompleted()` helper
+- Current events are emitted for upstream tool calls only, not internal tools
+
+**Decision**: Add activity emission in each internal tool handler:
+- Use `internal_tool_call` ActivityType (new constant)
+- Include tool_name field to identify which internal tool was called
+- Store query/arguments in the arguments field
+- Include existing intent metadata for call_tool_* handlers
+
+**Rationale**: Follows existing pattern for tool call activity emission.
+
+**Alternatives Considered**:
+1. Wrap all tool handlers with a decorator - Rejected: Overcomplicated, Go doesn't have decorators
+2. Use middleware on MCP server - Rejected: mark3labs/mcp-go doesn't support middleware pattern
+
+### 1.3 Config Change Events
+
+**Research Task**: Where are config changes made and how to capture them?
+
+**Findings**:
+- Server add/remove/update: `internal/management/server_manager.go`
+- Config file changes: `internal/config/watcher.go` hot reload
+- CLI operations: `cmd/mcpproxy/upstream_cmd.go`
+- REST API: `internal/httpapi/servers.go`
+- All modifications go through `storage.Manager` and emit `EventTypeServersChanged`
+
+**Decision**: Emit `config_change` activity when:
+- Server is added (via ServerManager.AddServer)
+- Server is removed (via ServerManager.RemoveServer)
+- Server is updated (via ServerManager.UpdateServer)
+- Use the existing `EventTypeServersChanged` subscription in ActivityService
+
+**Rationale**: ActivityService already subscribes to runtime events; extend it to handle config changes.
+
+**Alternatives Considered**:
+1. Emit from storage layer - Rejected: Storage doesn't have runtime access
+2. Emit from each CLI/API handler - Rejected: Duplication and easy to miss
+
+## 2. Activity Filter Multi-Type Support
+
+### 2.1 Backend Filter Logic
+
+**Research Task**: How does the current filter work?
+
+**Findings**:
+- `ActivityFilter` struct in `internal/storage/activity_models.go` (line 66)
+- Filter has single `Type string` field
+- `Matches()` method does exact string match (line 105)
+
+**Decision**: Change `Type` field to `Types []string` and update `Matches()`:
+- Accept comma-separated values in API/CLI
+- Parse into slice internally
+- Match if record type is in the slice (OR logic)
+- Empty slice means no type filter (all types)
+
+**Rationale**: Minimal API change, backwards compatible (single type still works).
+
+### 2.2 API Changes
+
+**Research Task**: How does the API currently handle type filter?
+
+**Findings**:
+- `GET /api/v1/activity` in `internal/httpapi/activity.go`
+- Query parameter `type` is parsed as single string
+- Passed to storage filter
+
+**Decision**:
+- Accept comma-separated values: `?type=tool_call,config_change`
+- Parse and split on comma
+- Document in OpenAPI spec
+
+**Rationale**: Standard pattern for multi-value query parameters.
+
+## 3. Web UI Enhancements
+
+### 3.1 Multi-Select Dropdown Component
+
+**Research Task**: What UI library is used?
+
+**Findings**:
+- Vue 3 + DaisyUI + Tailwind CSS
+- Current type filter in `Activity.vue` uses a simple `