Skip to content

Commit a35fb38

Browse files
committed
Merge origin/main into 013-structured-server-state
2 parents 7506805 + b32ea51 commit a35fb38

38 files changed

Lines changed: 3261 additions & 216 deletions

CLAUDE.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ See [docs/cli-management-commands.md](docs/cli-management-commands.md) for compl
6565
```bash
6666
mcpproxy activity list # List recent activity
6767
mcpproxy activity list --type tool_call --status error # Filter by type/status
68+
mcpproxy activity list --request-id <id> # Filter by HTTP request ID (for error correlation)
6869
mcpproxy activity watch # Real-time activity stream
6970
mcpproxy activity show <id> # View activity details
7071
mcpproxy activity summary # Show 24h statistics
@@ -201,6 +202,8 @@ See [docs/configuration.md](docs/configuration.md) for complete reference.
201202

202203
**Authentication**: Use `X-API-Key` header or `?apikey=` query parameter.
203204

205+
**Request ID Tracking**: All responses include `X-Request-Id` header. Error responses include `request_id` in JSON body. Use for log correlation: `mcpproxy activity list --request-id <id>`.
206+
204207
**Real-time Updates**:
205208
- `GET /events` - Server-Sent Events (SSE) stream for live updates
206209
- Streams both status changes and runtime events (`servers.changed`, `config.reloaded`)
@@ -385,6 +388,9 @@ See `docs/prerelease-builds.md` for download instructions.
385388
- BBolt database (`~/.mcpproxy/config.db`) - ActivityRecord extended with intent metadata (018-intent-declaration)
386389
- TypeScript 5.9, Vue 3.5, Go 1.24 (backend already exists) + Vue 3, Vue Router 4, Pinia 2, Tailwind CSS 3, DaisyUI 4, Vite 5 (019-activity-webui)
387390
- N/A (frontend consumes REST API from backend) (019-activity-webui)
391+
- Go 1.24 (toolchain go1.24.10) + Cobra (CLI), Chi router (HTTP), Zap (logging), mark3labs/mcp-go (MCP protocol) (020-oauth-login-feedback)
392+
- Go 1.24 (toolchain go1.24.10) + Cobra (CLI), Chi router (HTTP), Zap (logging), google/uuid (ID generation) (021-request-id-logging)
393+
- BBolt database (`~/.mcpproxy/config.db`) - activity log extended with request_id field (021-request-id-logging)
388394

389395
## Recent Changes
390396
- 001-update-version-display: Added Go 1.24 (toolchain go1.24.10)

cmd/mcpproxy/activity_cmd.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bufio"
55
"context"
66
"encoding/json"
7+
"errors"
78
"fmt"
89
"io"
910
"net/http"
@@ -37,6 +38,7 @@ var (
3738
activityLimit int
3839
activityOffset int
3940
activityIntentType string // Spec 018: Filter by operation type (read, write, destructive)
41+
activityRequestID string // Spec 021: Filter by HTTP request ID for correlation
4042
activityNoIcons bool // Disable emoji icons in output
4143

4244
// Show command flags
@@ -64,6 +66,7 @@ type ActivityFilter struct {
6466
Limit int
6567
Offset int
6668
IntentType string // Spec 018: Filter by operation type (read, write, destructive)
69+
RequestID string // Spec 021: Filter by HTTP request ID for correlation
6770
}
6871

6972
// Validate validates the filter options
@@ -168,6 +171,10 @@ func (f *ActivityFilter) ToQueryParams() url.Values {
168171
if f.IntentType != "" {
169172
q.Set("intent_type", f.IntentType)
170173
}
174+
// Spec 021: Add request_id filter for log correlation
175+
if f.RequestID != "" {
176+
q.Set("request_id", f.RequestID)
177+
}
171178
return q
172179
}
173180

@@ -369,14 +376,30 @@ func outputActivityError(err error, code string) error {
369376
return err
370377
}
371378

379+
// T026: Extract request_id from APIError if available
380+
var requestID string
381+
var apiErr *cliclient.APIError
382+
if errors.As(err, &apiErr) && apiErr.HasRequestID() {
383+
requestID = apiErr.RequestID
384+
}
385+
372386
if outputFormat == "json" || outputFormat == "yaml" {
373387
structErr := output.NewStructuredError(code, err.Error()).
374388
WithGuidance("Use 'mcpproxy activity list' to view recent activities").
375389
WithRecoveryCommand("mcpproxy activity list --limit 10")
390+
// T026: Add request_id to StructuredError if available
391+
if requestID != "" {
392+
structErr = structErr.WithRequestID(requestID)
393+
}
376394
result, _ := formatter.FormatError(structErr)
377395
fmt.Println(result)
378396
} else {
379397
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
398+
// T026: Include request ID with log retrieval suggestion if available
399+
if requestID != "" {
400+
fmt.Fprintf(os.Stderr, "\nRequest ID: %s\n", requestID)
401+
fmt.Fprintf(os.Stderr, "Use 'mcpproxy activity list --request-id %s' to find related logs.\n", requestID)
402+
}
380403
fmt.Fprintf(os.Stderr, "Hint: Use 'mcpproxy activity list' to view recent activities\n")
381404
}
382405
return err
@@ -405,6 +428,9 @@ Examples:
405428
# List errors from github server
406429
mcpproxy activity list --server github --status error
407430
431+
# List activity by request ID (for error correlation)
432+
mcpproxy activity list --request-id abc123-def456
433+
408434
# List activity as JSON
409435
mcpproxy activity list -o json`,
410436
RunE: runActivityList,
@@ -501,6 +527,7 @@ func init() {
501527
activityListCmd.Flags().IntVarP(&activityLimit, "limit", "n", 50, "Max records to return (1-100)")
502528
activityListCmd.Flags().IntVar(&activityOffset, "offset", 0, "Pagination offset")
503529
activityListCmd.Flags().StringVar(&activityIntentType, "intent-type", "", "Filter by intent operation type: read, write, destructive")
530+
activityListCmd.Flags().StringVar(&activityRequestID, "request-id", "", "Filter by HTTP request ID for log correlation")
504531
activityListCmd.Flags().BoolVar(&activityNoIcons, "no-icons", false, "Disable emoji icons in output (use text instead)")
505532

506533
// Watch command flags
@@ -585,6 +612,7 @@ func runActivityList(cmd *cobra.Command, _ []string) error {
585612
Limit: activityLimit,
586613
Offset: activityOffset,
587614
IntentType: activityIntentType,
615+
RequestID: activityRequestID,
588616
}
589617

590618
if err := filter.Validate(); err != nil {

cmd/mcpproxy/auth_cmd.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -486,7 +486,8 @@ func runAuthLoginClientMode(ctx context.Context, dataDir, serverName string) err
486486

487487
// Trigger OAuth via daemon
488488
if err := client.TriggerOAuthLogin(ctx, serverName); err != nil {
489-
return fmt.Errorf("failed to trigger OAuth login via daemon: %w", err)
489+
// T025: Use cliError to include request_id in error output
490+
return cliError("failed to trigger OAuth login via daemon", err)
490491
}
491492

492493
fmt.Printf("✅ OAuth authentication flow initiated successfully for server: %s\n", serverName)
@@ -617,7 +618,8 @@ func runAuthLogoutClientMode(ctx context.Context, dataDir, serverName string) er
617618

618619
// Trigger OAuth logout via daemon
619620
if err := client.TriggerOAuthLogout(ctx, serverName); err != nil {
620-
return fmt.Errorf("failed to trigger OAuth logout via daemon: %w", err)
621+
// T025: Use cliError to include request_id in error output
622+
return cliError("failed to trigger OAuth logout via daemon", err)
621623
}
622624

623625
fmt.Printf("✅ OAuth logout completed successfully for server: %s\n", serverName)

cmd/mcpproxy/call_cmd.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,8 @@ func runCallToolVariantClientMode(dataDir, toolVariant string, args map[string]i
435435
// For tool variants, we call the variant tool directly
436436
result, err := client.CallTool(callCtx, toolVariant, args)
437437
if err != nil {
438-
return fmt.Errorf("failed to call tool via daemon: %w", err)
438+
// T028: Use cliError to include request_id in error output
439+
return cliError("failed to call tool via daemon", err)
439440
}
440441

441442
// Output results based on format

cmd/mcpproxy/cmd_helpers.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,44 @@
11
package main
22

3+
import (
4+
"errors"
5+
"fmt"
6+
7+
"mcpproxy-go/internal/cliclient"
8+
)
9+
310
// cmd_helpers.go provides type-safe helper functions for extracting fields
411
// from JSON-decoded map[string]interface{} responses from the MCPProxy API.
512
//
613
// These functions are used throughout the CLI commands to safely extract
714
// typed values from API responses, handling missing keys and type mismatches
815
// gracefully by returning zero values.
916

17+
// formatErrorWithRequestID formats an error for CLI output, including request_id if available.
18+
// T023: Helper for CLI error display with request ID suggestion.
19+
func formatErrorWithRequestID(err error) string {
20+
if err == nil {
21+
return ""
22+
}
23+
24+
// Check if the error is an APIError with request_id
25+
var apiErr *cliclient.APIError
26+
if errors.As(err, &apiErr) && apiErr.HasRequestID() {
27+
return apiErr.FormatWithRequestID()
28+
}
29+
30+
// Fall back to standard error message
31+
return err.Error()
32+
}
33+
34+
// cliError returns a formatted error suitable for CLI output.
35+
// It includes request_id when available from API errors.
36+
// T023: Wrapper for CLI commands to display errors with request ID.
37+
func cliError(prefix string, err error) error {
38+
formattedMsg := formatErrorWithRequestID(err)
39+
return fmt.Errorf("%s: %s", prefix, formattedMsg)
40+
}
41+
1042
func getStringField(m map[string]interface{}, key string) string {
1143
if v, ok := m[key].(string); ok {
1244
return v

cmd/mcpproxy/code_cmd.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,8 @@ func runCodeExecClientMode(dataDir, code string, input map[string]interface{}, l
198198
codeAllowedSrvs,
199199
)
200200
if err != nil {
201-
fmt.Fprintf(os.Stderr, "Error calling daemon: %v\n", err)
201+
// T029: Use formatErrorWithRequestID to include request_id in error output
202+
fmt.Fprintf(os.Stderr, "Error calling daemon: %s\n", formatErrorWithRequestID(err))
202203
return exitError(1)
203204
}
204205

cmd/mcpproxy/tools_cmd.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,8 @@ func runToolsListClientMode(ctx context.Context, dataDir, serverName string, log
243243
// Fetch tools from daemon
244244
tools, err := client.GetServerTools(ctx, serverName)
245245
if err != nil {
246-
return fmt.Errorf("failed to get server tools from daemon: %w", err)
246+
// T027: Use cliError to include request_id in error output
247+
return cliError("failed to get server tools from daemon", err)
247248
}
248249

249250
// Output results

cmd/mcpproxy/upstream_cmd.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
44
"context"
55
"encoding/json"
6+
"errors"
67
"fmt"
78
"os"
89
"os/exec"
@@ -399,9 +400,17 @@ func outputServers(servers []map[string]interface{}) error {
399400
// outputError formats and outputs an error based on the current output format.
400401
// For structured formats (json, yaml), it outputs a StructuredError.
401402
// For table format, it outputs a human-readable error message to stderr.
403+
// T023: Updated to extract request_id from APIError for log correlation
402404
func outputError(err error, code string) error {
403405
outputFormat := ResolveOutputFormat()
404406

407+
// T023: Extract request_id from APIError if available
408+
var requestID string
409+
var apiErr *cliclient.APIError
410+
if errors.As(err, &apiErr) && apiErr.HasRequestID() {
411+
requestID = apiErr.RequestID
412+
}
413+
405414
// Convert to StructuredError if not already
406415
var structErr output.StructuredError
407416
if se, ok := err.(output.StructuredError); ok {
@@ -410,6 +419,11 @@ func outputError(err error, code string) error {
410419
structErr = output.NewStructuredError(code, err.Error())
411420
}
412421

422+
// T023: Add request_id to StructuredError if available
423+
if requestID != "" {
424+
structErr = structErr.WithRequestID(requestID)
425+
}
426+
413427
// For structured formats, output JSON/YAML error to stdout
414428
if outputFormat == "json" || outputFormat == "yaml" {
415429
formatter, fmtErr := GetOutputFormatter()
@@ -430,7 +444,14 @@ func outputError(err error, code string) error {
430444
}
431445

432446
// For table format, output human-readable error to stderr
433-
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
447+
// T023: Include request ID with log retrieval suggestion if available
448+
if requestID != "" {
449+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
450+
fmt.Fprintf(os.Stderr, "\nRequest ID: %s\n", requestID)
451+
fmt.Fprintf(os.Stderr, "Use 'mcpproxy activity list --request-id %s' to find related logs.\n", requestID)
452+
} else {
453+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
454+
}
434455
return err
435456
}
436457

0 commit comments

Comments
 (0)