Skip to content

Commit c6072be

Browse files
committed
Enhance logging system and add tail_log operation
- Updated logging configuration to support custom log directories and filenames. - Introduced `tail_log` operation for reading recent log entries from upstream servers, aiding in debugging. - Improved log structure and content for better traceability of MCP server interactions. - Adjusted CLI commands and documentation to reflect new logging features and configurations.
1 parent 7969ff6 commit c6072be

9 files changed

Lines changed: 362 additions & 25 deletions

File tree

DESIGN.md

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,12 @@ Re‑implement the existing Python MCP proxy in Go, delivering a single‑binary
2424

2525
* WebSocket/stdin transports, distributed indexing, vector search.
2626

27-
## 3  Tech Stack
27+
## 3  Tech Stack
2828

2929
| Concern | Library | Reason |
3030
| ----------------- | --------------------------------- | ---------------------------------- |
3131
| MCP server/client | **`mark3labs/mcp-go`** | Native Go, Streamable‑HTTP support |
32-
| Full‑text search | **Bleve v2** | Embeddable BM25 |
32+
| Full‑text search | **Bleve v2** | Embeddable BM25 |
3333
| CLI & config | **`spf13/cobra` + `spf13/viper`** | Flags → env → file binding |
3434
| Persistence | **bbolt** | Single‑file ACID |
3535
| Sys‑tray | **`fyne.io/systray`** | Tiny cross‑platform tray |
@@ -66,7 +66,7 @@ Re‑implement the existing Python MCP proxy in Go, delivering a single‑binary
6666
* Stored alongside tool metadata in both Bleve doc and Bolt `toolhash` bucket.
6767
* During re‑sync `ListTools` results are hashed; unchanged hashes skip re‑indexing.
6868

69-
## 6  Data Model (bbolt)
69+
## 6  Data Model (bbolt)
7070

7171
| Bucket | Key | Value |
7272
| ----------- | ------------- | ---------------------------- |
@@ -119,7 +119,22 @@ Input: {"operation":"add","url":"https://api.mcp.dev","name":"dev"}
119119
Output: {"id":"uuid","enabled":true}
120120
```
121121

122-
Operations: `list` / `add` / `remove` / `update`.
122+
Operations: `list` / `add` / `remove` / `update` / `tail_log`.
123+
124+
#### 8.3.1 `tail_log` Operation
125+
126+
The `tail_log` operation allows LLMs to read recent log entries from a specific upstream server for debugging purposes:
127+
128+
```jsonc
129+
Input: {"operation":"tail_log","name":"dev","lines":50}
130+
Output: {"server":"dev","lines":50,"log_entries":[...]}
131+
```
132+
133+
**Parameters:**
134+
- `name` (required): Server name to read logs from
135+
- `lines` (optional): Number of recent lines to return (default: 50, max: 500)
136+
137+
**Use Case:** Enables AI agents to autonomously diagnose connection issues, authentication failures, and other upstream server problems by reading recent log entries.
123138

124139
### 8.4  `tools_stat`
125140

@@ -134,10 +149,55 @@ Returns `{total_tools, top:[{tool_name,count}]}`.
134149

135150
## 10  CLI, Config & Tray
136151

137-
* `mcpproxy [--listen :8080] [--data-dir ~/.mcpproxy] [--upstream "prod=https://api"]`
152+
* `mcpproxy [--listen :8080] [--log-dir ~/.mcpproxy/logs] [--upstream "prod=https://api"]`
138153
* Viper reads `$MCPP_` envs and `config.toml`.
139154
* Tray (systray): icon + menu items (Enable, Disable, Add…, Reindex, Quit).
140155

156+
### 10.1 Logging System
157+
158+
#### Per-Upstream Server Logging
159+
160+
mcpproxy implements comprehensive per-upstream-server logging to facilitate debugging of connection issues and MCP communication problems.
161+
162+
**Log File Structure:**
163+
```
164+
~/.mcpproxy/logs/
165+
├── main.log # Main application log
166+
├── server-github.log # GitHub MCP server interactions
167+
├── server-filesystem.log # Filesystem MCP server interactions
168+
└── server-database.log # Database MCP server interactions
169+
```
170+
171+
**Log Content:**
172+
- **MCP Protocol Messages**: All JSON-RPC messages between mcpproxy and upstream servers
173+
- **Connection Events**: Connect, disconnect, retry attempts, and failures
174+
- **Authentication**: OAuth flows, token refreshes, and auth errors
175+
- **Process Output**: STDERR from stdio-based MCP servers
176+
- **Timing Information**: Request/response latencies and timeout events
177+
178+
**Example Log Format:**
179+
```
180+
2025-06-04T03:42:38.375Z [github] [info] Connecting to upstream server
181+
2025-06-04T03:42:38.630Z [github] [info] Connected successfully
182+
2025-06-04T03:42:38.663Z [github] [debug] [Client→Server] initialize
183+
2025-06-04T03:42:38.663Z [github] [debug] {"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"mcpproxy-go","version":"1.0.0"}},"jsonrpc":"2.0","id":0}
184+
2025-06-04T03:42:38.700Z [github] [debug] [Server→Client] 0
185+
2025-06-04T03:42:38.700Z [github] [debug] {"jsonrpc":"2.0","id":0,"result":{"protocolVersion":"2024-11-05","capabilities":{"tools":{"listChanged":false}},"serverInfo":{"name":"GitHub MCP Server","version":"1.0.0"}}}
186+
2025-06-04T03:42:41.670Z [github] [error] Connection error: HTTP 500 - Internal Server Error
187+
2025-06-04T03:42:41.671Z [github] [info] Attempting reconnection in 5 seconds...
188+
```
189+
190+
**CLI Configuration:**
191+
- `--log-dir`: Specify custom log directory (default: OS-specific standard location)
192+
- `--log-level`: Set log level for all loggers (debug, info, warn, error)
193+
- Main application log: `main.log`
194+
- Per-server logs: `server-{name}.log`
195+
196+
**Log Rotation:**
197+
- Automatic rotation based on file size (10MB default)
198+
- Configurable retention (5 backup files, 30 days default)
199+
- Optional compression for rotated files
200+
141201
## 11  Build & Packaging
142202

143203
* Cross‑compile via `GOOS=darwin/windows/linux`.
@@ -171,7 +231,10 @@ The proxy supports dynamic management of upstream MCP servers through the `upstr
171231
~/.mcpproxy/
172232
├── mcp_config.json # Main configuration file
173233
├── data.bolt # BoltDB storage (tool stats, metadata)
174-
└── index.bleve/ # Search index directory
234+
├── index.bleve/ # Search index directory
235+
└── logs/ # Log files directory
236+
├── main.log # Main application log
237+
└── server-*.log # Per-upstream server logs
175238
```
176239

177240
#### Configuration Flow

cmd/mcpproxy/main.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ var (
2525
debugSearch bool
2626
toolResponseLimit int
2727
logToFile bool
28-
logFile string
28+
logDir string
2929

3030
// Security flags
3131
readOnlyMode bool
@@ -61,7 +61,7 @@ func main() {
6161
rootCmd.PersistentFlags().BoolVar(&debugSearch, "debug-search", false, "Enable debug search tool for search relevancy debugging")
6262
rootCmd.PersistentFlags().IntVar(&toolResponseLimit, "tool-response-limit", 0, "Tool response limit in characters (0 = disabled, default: 20000 from config)")
6363
rootCmd.PersistentFlags().BoolVar(&logToFile, "log-to-file", true, "Enable logging to file in standard OS location")
64-
rootCmd.PersistentFlags().StringVar(&logFile, "log-file", "", "Custom log file path (overrides standard OS location)")
64+
rootCmd.PersistentFlags().StringVar(&logDir, "log-dir", "", "Custom log directory path (overrides standard OS location)")
6565
rootCmd.PersistentFlags().BoolVar(&readOnlyMode, "read-only", false, "Enable read-only mode")
6666
rootCmd.PersistentFlags().BoolVar(&disableManagement, "disable-management", false, "Disable management features")
6767
rootCmd.PersistentFlags().BoolVar(&allowServerAdd, "allow-server-add", true, "Allow adding new servers")
@@ -87,7 +87,7 @@ func runServer(_ *cobra.Command, _ []string) error {
8787
Level: logLevel,
8888
EnableFile: logToFile,
8989
EnableConsole: true,
90-
Filename: "mcpproxy.log",
90+
Filename: "main.log",
9191
MaxSize: 10,
9292
MaxBackups: 5,
9393
MaxAge: 30,
@@ -98,11 +98,16 @@ func runServer(_ *cobra.Command, _ []string) error {
9898
// Override specific fields from command line
9999
cfg.Logging.Level = logLevel
100100
cfg.Logging.EnableFile = logToFile
101-
if logFile != "" {
102-
cfg.Logging.Filename = logFile
101+
if cfg.Logging.Filename == "" || cfg.Logging.Filename == "mcpproxy.log" {
102+
cfg.Logging.Filename = "main.log"
103103
}
104104
}
105105

106+
// Override log directory if specified
107+
if logDir != "" {
108+
cfg.Logging.LogDir = logDir
109+
}
110+
106111
// Setup logger with new logging system
107112
logger, err := logs.SetupLogger(cfg.Logging)
108113
if err != nil {

internal/config/config.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,10 @@ type LogConfig struct {
3939
EnableFile bool `json:"enable_file" mapstructure:"enable-file"`
4040
EnableConsole bool `json:"enable_console" mapstructure:"enable-console"`
4141
Filename string `json:"filename" mapstructure:"filename"`
42-
MaxSize int `json:"max_size" mapstructure:"max-size"` // MB
43-
MaxBackups int `json:"max_backups" mapstructure:"max-backups"` // number of backup files
44-
MaxAge int `json:"max_age" mapstructure:"max-age"` // days
42+
LogDir string `json:"log_dir,omitempty" mapstructure:"log-dir"` // Custom log directory
43+
MaxSize int `json:"max_size" mapstructure:"max-size"` // MB
44+
MaxBackups int `json:"max_backups" mapstructure:"max-backups"` // number of backup files
45+
MaxAge int `json:"max_age" mapstructure:"max-age"` // days
4546
Compress bool `json:"compress" mapstructure:"compress"`
4647
JSONFormat bool `json:"json_format" mapstructure:"json-format"`
4748
}
@@ -158,7 +159,7 @@ func DefaultConfig() *Config {
158159
Level: "info",
159160
EnableFile: true,
160161
EnableConsole: true,
161-
Filename: "mcpproxy.log",
162+
Filename: "main.log",
162163
MaxSize: 10, // 10MB
163164
MaxBackups: 5, // 5 backup files
164165
MaxAge: 30, // 30 days

internal/logs/logger.go

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package logs
22

33
import (
4+
"bufio"
45
"fmt"
56
"io"
67
"mcpproxy-go/internal/config"
@@ -85,8 +86,8 @@ func SetupLogger(config *config.LogConfig) (*zap.Logger, error) {
8586

8687
// createFileCore creates a file-based logging core
8788
func createFileCore(config *config.LogConfig, level zapcore.Level) (zapcore.Core, error) {
88-
// Get log file path
89-
logFilePath, err := GetLogFilePath(config.Filename)
89+
// Get log file path with custom directory support
90+
logFilePath, err := GetLogFilePathWithDir(config.LogDir, config.Filename)
9091
if err != nil {
9192
return nil, fmt.Errorf("failed to get log file path: %w", err)
9293
}
@@ -210,3 +211,89 @@ func CleanupTestWriter(file *os.File) error {
210211
}
211212
return nil
212213
}
214+
215+
// CreateUpstreamServerLogger creates a logger for a specific upstream server
216+
func CreateUpstreamServerLogger(config *config.LogConfig, serverName string) (*zap.Logger, error) {
217+
if config == nil {
218+
config = DefaultLogConfig()
219+
}
220+
221+
// Create a copy of the config for the upstream server
222+
serverConfig := *config
223+
serverConfig.Filename = fmt.Sprintf("server-%s.log", serverName)
224+
serverConfig.EnableConsole = false // Upstream servers only log to file
225+
226+
// Parse log level
227+
var level zapcore.Level
228+
switch serverConfig.Level {
229+
case "debug":
230+
level = zap.DebugLevel
231+
case "info":
232+
level = zap.InfoLevel
233+
case "warn":
234+
level = zap.WarnLevel
235+
case "error":
236+
level = zap.ErrorLevel
237+
default:
238+
level = zap.InfoLevel
239+
}
240+
241+
// Create file core for upstream server
242+
fileCore, err := createFileCore(&serverConfig, level)
243+
if err != nil {
244+
return nil, fmt.Errorf("failed to create file core for upstream server %s: %w", serverName, err)
245+
}
246+
247+
// Create logger with server name prefix
248+
logger := zap.New(fileCore, zap.AddCaller(), zap.AddCallerSkip(1))
249+
logger = logger.With(zap.String("server", serverName))
250+
251+
return logger, nil
252+
}
253+
254+
// ReadUpstreamServerLogTail reads the last N lines from an upstream server log file
255+
func ReadUpstreamServerLogTail(config *config.LogConfig, serverName string, lines int) ([]string, error) {
256+
if lines <= 0 {
257+
lines = 50
258+
}
259+
if lines > 500 {
260+
lines = 500
261+
}
262+
263+
// Get log file path
264+
filename := fmt.Sprintf("server-%s.log", serverName)
265+
logFilePath, err := GetLogFilePathWithDir(config.LogDir, filename)
266+
if err != nil {
267+
return nil, fmt.Errorf("failed to get log file path for server %s: %w", serverName, err)
268+
}
269+
270+
// Check if file exists
271+
if _, err := os.Stat(logFilePath); os.IsNotExist(err) {
272+
return []string{}, nil // Return empty slice if file doesn't exist
273+
}
274+
275+
// Read file
276+
file, err := os.Open(logFilePath)
277+
if err != nil {
278+
return nil, fmt.Errorf("failed to open log file for server %s: %w", serverName, err)
279+
}
280+
defer file.Close()
281+
282+
// Read all lines
283+
var allLines []string
284+
scanner := bufio.NewScanner(file)
285+
for scanner.Scan() {
286+
allLines = append(allLines, scanner.Text())
287+
}
288+
289+
if err := scanner.Err(); err != nil {
290+
return nil, fmt.Errorf("failed to read log file for server %s: %w", serverName, err)
291+
}
292+
293+
// Return last N lines
294+
if len(allLines) <= lines {
295+
return allLines, nil
296+
}
297+
298+
return allLines[len(allLines)-lines:], nil
299+
}

internal/logs/paths.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"os"
55
"path/filepath"
66
"runtime"
7+
"strings"
78
)
89

910
const (
@@ -104,6 +105,28 @@ func GetLogFilePath(filename string) (string, error) {
104105
return filepath.Join(logDir, filename), nil
105106
}
106107

108+
// GetLogFilePathWithDir returns the full path for a log file in a custom log directory
109+
func GetLogFilePathWithDir(logDir, filename string) (string, error) {
110+
if logDir == "" {
111+
return GetLogFilePath(filename)
112+
}
113+
114+
// Expand user home directory if needed
115+
if strings.HasPrefix(logDir, "~/") {
116+
homeDir, err := os.UserHomeDir()
117+
if err != nil {
118+
return "", err
119+
}
120+
logDir = filepath.Join(homeDir, logDir[2:])
121+
}
122+
123+
if err := EnsureLogDir(logDir); err != nil {
124+
return "", err
125+
}
126+
127+
return filepath.Join(logDir, filename), nil
128+
}
129+
107130
// LogDirInfo returns information about the log directory for different OS
108131
type LogDirInfo struct {
109132
Path string `json:"path"`

0 commit comments

Comments
 (0)