Skip to content

Commit 3859e33

Browse files
Copilotlpcox
andcommitted
Implement ToolsLogger with tests and integration
Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
1 parent 28286b6 commit 3859e33

5 files changed

Lines changed: 433 additions & 2 deletions

File tree

internal/cmd/root.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ func postRun(cmd *cobra.Command, args []string) {
143143
logger.CloseMarkdownLogger()
144144
logger.CloseJSONLLogger()
145145
logger.CloseServerFileLogger()
146+
logger.CloseToolsLogger()
146147
logger.CloseGlobalLogger()
147148
}
148149

@@ -171,6 +172,11 @@ func run(cmd *cobra.Command, args []string) error {
171172
log.Printf("Warning: Failed to initialize JSONL logger: %v", err)
172173
}
173174

175+
// Initialize tools logger for tracking available tools
176+
if err := logger.InitToolsLogger(logDir, "tools.json"); err != nil {
177+
log.Printf("Warning: Failed to initialize tools logger: %v", err)
178+
}
179+
174180
logger.LogInfoMd("startup", "MCPG Gateway version: %s", cliVersion)
175181

176182
// Log config source based on what was provided

internal/logger/global_helpers.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ package logger
1717
import "sync"
1818

1919
// closableLogger is a constraint for types that have a Close method.
20-
// This is satisfied by *FileLogger, *JSONLLogger, *MarkdownLogger, and *ServerFileLogger.
20+
// This is satisfied by *FileLogger, *JSONLLogger, *MarkdownLogger, *ServerFileLogger, and *ToolsLogger.
2121
type closableLogger interface {
22-
*FileLogger | *JSONLLogger | *MarkdownLogger | *ServerFileLogger
22+
*FileLogger | *JSONLLogger | *MarkdownLogger | *ServerFileLogger | *ToolsLogger
2323
Close() error
2424
}
2525

internal/logger/tools_logger.go

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
// Package logger provides structured logging for the MCP Gateway.
2+
//
3+
// This file implements logging of MCP server tools to a JSON file (tools.json).
4+
// It maintains a mapping of server IDs to their available tools with names and descriptions.
5+
package logger
6+
7+
import (
8+
"encoding/json"
9+
"fmt"
10+
"log"
11+
"os"
12+
"path/filepath"
13+
"sync"
14+
)
15+
16+
// ToolInfo represents information about a single tool
17+
type ToolInfo struct {
18+
Name string `json:"name"`
19+
Description string `json:"description"`
20+
}
21+
22+
// ToolsData represents the structure of tools.json
23+
type ToolsData struct {
24+
// Map of serverID to array of tools
25+
Servers map[string][]ToolInfo `json:"servers"`
26+
}
27+
28+
// ToolsLogger manages logging of MCP server tools to a JSON file
29+
type ToolsLogger struct {
30+
logDir string
31+
fileName string
32+
data *ToolsData
33+
mu sync.Mutex
34+
useFallback bool
35+
}
36+
37+
var (
38+
globalToolsLogger *ToolsLogger
39+
globalToolsMu sync.RWMutex
40+
)
41+
42+
// InitToolsLogger initializes the global tools logger
43+
// If the log directory doesn't exist and can't be created, falls back to no-op
44+
func InitToolsLogger(logDir, fileName string) error {
45+
logger, err := initLogger(
46+
logDir, fileName, os.O_TRUNC, // Truncate existing file to start fresh
47+
// Setup function: configure the logger after directory is ready
48+
func(file *os.File, logDir, fileName string) (*ToolsLogger, error) {
49+
// Close the file immediately - we'll write directly later
50+
if file != nil {
51+
file.Close()
52+
}
53+
54+
tl := &ToolsLogger{
55+
logDir: logDir,
56+
fileName: fileName,
57+
data: &ToolsData{
58+
Servers: make(map[string][]ToolInfo),
59+
},
60+
}
61+
log.Printf("Tools logging to file: %s", filepath.Join(logDir, fileName))
62+
return tl, nil
63+
},
64+
// Error handler: fallback to no-op on error
65+
func(err error, logDir, fileName string) (*ToolsLogger, error) {
66+
log.Printf("WARNING: Failed to initialize tools log file: %v", err)
67+
log.Printf("WARNING: Tools logging disabled")
68+
tl := &ToolsLogger{
69+
logDir: logDir,
70+
fileName: fileName,
71+
useFallback: true,
72+
data: &ToolsData{
73+
Servers: make(map[string][]ToolInfo),
74+
},
75+
}
76+
return tl, nil
77+
},
78+
)
79+
80+
initGlobalToolsLogger(logger)
81+
return err
82+
}
83+
84+
// LogTools logs the tools for a specific server
85+
func (tl *ToolsLogger) LogTools(serverID string, tools []ToolInfo) error {
86+
tl.mu.Lock()
87+
defer tl.mu.Unlock()
88+
89+
if tl.useFallback {
90+
return nil // Silently skip if in fallback mode
91+
}
92+
93+
// Update the data structure
94+
tl.data.Servers[serverID] = tools
95+
96+
// Write the updated data to file
97+
return tl.writeToFile()
98+
}
99+
100+
// writeToFile writes the current tools data to the JSON file
101+
// Caller must hold tl.mu lock
102+
func (tl *ToolsLogger) writeToFile() error {
103+
filePath := filepath.Join(tl.logDir, tl.fileName)
104+
105+
// Marshal to JSON with indentation for readability
106+
jsonData, err := json.MarshalIndent(tl.data, "", " ")
107+
if err != nil {
108+
return fmt.Errorf("failed to marshal tools data: %w", err)
109+
}
110+
111+
// Write to file atomically using a temp file + rename
112+
tempPath := filePath + ".tmp"
113+
if err := os.WriteFile(tempPath, jsonData, 0644); err != nil {
114+
return fmt.Errorf("failed to write temp file: %w", err)
115+
}
116+
117+
if err := os.Rename(tempPath, filePath); err != nil {
118+
// Clean up temp file on error
119+
os.Remove(tempPath)
120+
return fmt.Errorf("failed to rename temp file: %w", err)
121+
}
122+
123+
return nil
124+
}
125+
126+
// Close is a no-op for ToolsLogger (implements closableLogger interface)
127+
func (tl *ToolsLogger) Close() error {
128+
// No file handle to close since we write directly each time
129+
return nil
130+
}
131+
132+
// Global logging function that uses the global tools logger
133+
134+
// LogToolsForServer logs the tools for a specific server
135+
func LogToolsForServer(serverID string, tools []ToolInfo) {
136+
globalToolsMu.RLock()
137+
defer globalToolsMu.RUnlock()
138+
139+
if globalToolsLogger != nil {
140+
if err := globalToolsLogger.LogTools(serverID, tools); err != nil {
141+
// Log errors using the standard logger to avoid recursion
142+
log.Printf("WARNING: Failed to log tools for server %s: %v", serverID, err)
143+
}
144+
}
145+
}
146+
147+
// CloseToolsLogger closes the global tools logger
148+
func CloseToolsLogger() error {
149+
return closeGlobalToolsLogger()
150+
}
151+
152+
// initGlobalToolsLogger initializes the global ToolsLogger using the generic helper.
153+
func initGlobalToolsLogger(logger *ToolsLogger) {
154+
initGlobalLogger(&globalToolsMu, &globalToolsLogger, logger)
155+
}
156+
157+
// closeGlobalToolsLogger closes the global ToolsLogger using the generic helper.
158+
func closeGlobalToolsLogger() error {
159+
return closeGlobalLogger(&globalToolsMu, &globalToolsLogger)
160+
}

0 commit comments

Comments
 (0)