Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,7 @@ DEBUG_COLORS=0 DEBUG=* ./awmg --config config.toml
- `{serverID}.log` - Per-server logs (e.g., `github.log`, `slack.log`) for easier troubleshooting
- `gateway.md` - Markdown-formatted logs for GitHub workflow previews
- `rpc-messages.jsonl` - Machine-readable JSONL format for RPC message analysis
- `tools.json` - Available tools from all backend MCP servers (mapping server IDs to their tool names and descriptions)
- Logs include: startup, client interactions, backend operations, auth events, errors

**Per-ServerID Logging:**
Expand All @@ -406,6 +407,29 @@ DEBUG_COLORS=0 DEBUG=* ./awmg --config config.toml
- The `payloadPreview` shows the first 500 characters of the JSON for quick reference
- To access the full data with all actual values, read the JSON file at `payloadPath`

**Tools Catalog (tools.json):**
- The gateway maintains a catalog of all available tools from backend MCP servers in `tools.json`
- Located in the log directory (e.g., `/tmp/gh-aw/mcp-logs/tools.json`)
- Updated automatically during gateway startup when backend servers are registered
- Format: JSON mapping of server IDs to arrays of tool information
- Each tool includes: `name` (tool name without server prefix) and `description`
- Example structure:
```json
{
"servers": {
"github": [
{"name": "search_code", "description": "Search for code in repositories"},
{"name": "get_file_contents", "description": "Get the contents of a file"}
],
"slack": [
{"name": "send_message", "description": "Send a message to a Slack channel"}
]
}
}
```
- Useful for discovering available tools across all configured backend servers
- Can be used by clients or monitoring tools to understand gateway capabilities

## Error Debugging

**Enhanced Error Context**: Command failures include:
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ For detailed setup instructions, building from source, and local development, se
- `{serverID}.log`: Per-server logs for easier troubleshooting
- `gateway.md`: Markdown-formatted logs for GitHub workflow previews
- `rpc-messages.jsonl`: Machine-readable RPC message logs
- `tools.json`: Available tools from all backend MCP servers
- `-p 8000:8000`: Port mapping must match `MCP_GATEWAY_PORT`

MCPG will start in routed mode on `http://0.0.0.0:8000` (using `MCP_GATEWAY_PORT`), proxying MCP requests to your configured backend servers.
Expand Down Expand Up @@ -400,6 +401,7 @@ The gateway creates multiple log files for different purposes:
2. **`{serverID}.log`** - Per-server logs (e.g., `github.log`, `slack.log`) for easier troubleshooting of specific backend servers
3. **`gateway.md`** - Markdown-formatted logs for GitHub workflow previews
4. **`rpc-messages.jsonl`** - Machine-readable JSONL format for RPC message analysis
5. **`tools.json`** - Available tools from all backend MCP servers (mapping server IDs to their tool names and descriptions)

### Log File Location

Expand Down Expand Up @@ -427,7 +429,8 @@ Example log directory structure:
├── slack.log # Only Slack server logs
├── notion.log # Only Notion server logs
├── gateway.md # Markdown format
└── rpc-messages.jsonl # RPC messages
├── rpc-messages.jsonl # RPC messages
└── tools.json # Available tools
```

**Using the environment variable:**
Expand Down
6 changes: 6 additions & 0 deletions internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ func postRun(cmd *cobra.Command, args []string) {
logger.CloseMarkdownLogger()
logger.CloseJSONLLogger()
logger.CloseServerFileLogger()
logger.CloseToolsLogger()
logger.CloseGlobalLogger()
}

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

// Initialize tools logger for tracking available tools
if err := logger.InitToolsLogger(logDir, "tools.json"); err != nil {
log.Printf("Warning: Failed to initialize tools logger: %v", err)
}

logger.LogInfoMd("startup", "MCPG Gateway version: %s", cliVersion)

// Log config source based on what was provided
Expand Down
4 changes: 2 additions & 2 deletions internal/logger/global_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ package logger
import "sync"

// closableLogger is a constraint for types that have a Close method.
// This is satisfied by *FileLogger, *JSONLLogger, *MarkdownLogger, and *ServerFileLogger.
// This is satisfied by *FileLogger, *JSONLLogger, *MarkdownLogger, *ServerFileLogger, and *ToolsLogger.
type closableLogger interface {
*FileLogger | *JSONLLogger | *MarkdownLogger | *ServerFileLogger
*FileLogger | *JSONLLogger | *MarkdownLogger | *ServerFileLogger | *ToolsLogger
Close() error
}

Expand Down
160 changes: 160 additions & 0 deletions internal/logger/tools_logger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// Package logger provides structured logging for the MCP Gateway.
//
// This file implements logging of MCP server tools to a JSON file (tools.json).
// It maintains a mapping of server IDs to their available tools with names and descriptions.
package logger

import (
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"sync"
)

// ToolInfo represents information about a single tool
type ToolInfo struct {
Name string `json:"name"`
Description string `json:"description"`
}

// ToolsData represents the structure of tools.json
type ToolsData struct {
// Map of serverID to array of tools
Servers map[string][]ToolInfo `json:"servers"`
}

// ToolsLogger manages logging of MCP server tools to a JSON file
type ToolsLogger struct {
logDir string
fileName string
data *ToolsData
mu sync.Mutex
useFallback bool
}

var (
globalToolsLogger *ToolsLogger
globalToolsMu sync.RWMutex
)

// InitToolsLogger initializes the global tools logger
// If the log directory doesn't exist and can't be created, falls back to no-op
func InitToolsLogger(logDir, fileName string) error {
logger, err := initLogger(
logDir, fileName, os.O_TRUNC, // Truncate existing file to start fresh
// Setup function: configure the logger after directory is ready
func(file *os.File, logDir, fileName string) (*ToolsLogger, error) {
// Close the file immediately - we'll write directly later
if file != nil {
file.Close()
}

tl := &ToolsLogger{
logDir: logDir,
fileName: fileName,
data: &ToolsData{
Servers: make(map[string][]ToolInfo),
},
}
log.Printf("Tools logging to file: %s", filepath.Join(logDir, fileName))
return tl, nil
},
Comment on lines +44 to +63
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

InitToolsLogger truncates/creates tools.json via initLogger but the setup function closes the file and never writes an initial JSON payload. This leaves an empty (invalid JSON) tools.json until the first LogTools() call, and can permanently leave an invalid file if startup exits before tools are registered. Consider writing an initial { "servers": {} } to disk during initialization (or avoid truncating until the first successful write) so the file is always valid JSON.

Copilot uses AI. Check for mistakes.
// Error handler: fallback to no-op on error
func(err error, logDir, fileName string) (*ToolsLogger, error) {
log.Printf("WARNING: Failed to initialize tools log file: %v", err)
log.Printf("WARNING: Tools logging disabled")
tl := &ToolsLogger{
logDir: logDir,
fileName: fileName,
useFallback: true,
data: &ToolsData{
Servers: make(map[string][]ToolInfo),
},
}
return tl, nil
},
)

initGlobalToolsLogger(logger)
return err
}

// LogTools logs the tools for a specific server
func (tl *ToolsLogger) LogTools(serverID string, tools []ToolInfo) error {
tl.mu.Lock()
defer tl.mu.Unlock()

if tl.useFallback {
return nil // Silently skip if in fallback mode
}

// Update the data structure
tl.data.Servers[serverID] = tools

// Write the updated data to file
return tl.writeToFile()
}

// writeToFile writes the current tools data to the JSON file
// Caller must hold tl.mu lock
func (tl *ToolsLogger) writeToFile() error {
filePath := filepath.Join(tl.logDir, tl.fileName)

// Marshal to JSON with indentation for readability
jsonData, err := json.MarshalIndent(tl.data, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal tools data: %w", err)
}

// Write to file atomically using a temp file + rename
tempPath := filePath + ".tmp"
if err := os.WriteFile(tempPath, jsonData, 0644); err != nil {
return fmt.Errorf("failed to write temp file: %w", err)
}

if err := os.Rename(tempPath, filePath); err != nil {
// Clean up temp file on error
os.Remove(tempPath)
return fmt.Errorf("failed to rename temp file: %w", err)
}

return nil
}

// Close is a no-op for ToolsLogger (implements closableLogger interface)
func (tl *ToolsLogger) Close() error {
// No file handle to close since we write directly each time
return nil
}

// Global logging function that uses the global tools logger

// LogToolsForServer logs the tools for a specific server
func LogToolsForServer(serverID string, tools []ToolInfo) {
globalToolsMu.RLock()
defer globalToolsMu.RUnlock()

if globalToolsLogger != nil {
if err := globalToolsLogger.LogTools(serverID, tools); err != nil {
// Log errors using the standard logger to avoid recursion
log.Printf("WARNING: Failed to log tools for server %s: %v", serverID, err)
}
}
}

// CloseToolsLogger closes the global tools logger
func CloseToolsLogger() error {
return closeGlobalToolsLogger()
}

// initGlobalToolsLogger initializes the global ToolsLogger using the generic helper.
func initGlobalToolsLogger(logger *ToolsLogger) {
initGlobalLogger(&globalToolsMu, &globalToolsLogger, logger)
}

// closeGlobalToolsLogger closes the global ToolsLogger using the generic helper.
func closeGlobalToolsLogger() error {
return closeGlobalLogger(&globalToolsMu, &globalToolsLogger)
}
Loading
Loading