Skip to content

Add tools.json logging for MCP server tool discovery#903

Merged
lpcox merged 4 commits intomainfrom
copilot/collect-tools-responses
Feb 11, 2026
Merged

Add tools.json logging for MCP server tool discovery#903
lpcox merged 4 commits intomainfrom
copilot/collect-tools-responses

Conversation

Copy link
Contributor

Copilot AI commented Feb 11, 2026

Implements automatic collection of available tools from backend MCP servers into tools.json in the log directory. Maps server IDs to tool arrays containing name and description.

Implementation

  • ToolsLogger (internal/logger/tools_logger.go): Thread-safe JSON file writer with atomic updates and graceful fallback
  • Gateway integration: Initialize on startup, capture tools from tools/list responses in unified.go, cleanup on shutdown
  • Data structure: Original tool names (pre-prefix) with descriptions per server

Output Format

{
  "servers": {
    "github": [
      {"name": "search_code", "description": "Fast code search across repositories"},
      {"name": "get_file_contents", "description": "Get file contents from repository"}
    ],
    "slack": [
      {"name": "send_message", "description": "Send message to channel"}
    ]
  }
}

Use Cases

  • Tool discovery without RPC calls
  • Capability auditing and monitoring
  • Client configuration and documentation

Testing

  • Unit tests: initialization, multi-server, updates, edge cases
  • Integration test: end-to-end with mock MCP servers

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • example.com
    • Triggering command: /tmp/go-build801673111/b275/launcher.test /tmp/go-build801673111/b275/launcher.test -test.testlogfile=/tmp/go-build801673111/b275/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true g_.a --global .12/x64/as credential.helpe/opt/hostedtoolcache/go/1.25.6/x64/pkg/tool/linux_amd64/compile go (dns block)
  • invalid-host-that-does-not-exist-12345.com
    • Triggering command: /tmp/go-build801673111/b260/config.test /tmp/go-build801673111/b260/config.test -test.testlogfile=/tmp/go-build801673111/b260/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true -c=4 -nolocalimports -importcfg /tmp/go-build4280343458/b076/importcfg -pack /home/REDACTED/work/gh-aw-mcpg/gh-aw-mcpg/internal/logger/common.go /home/REDACTED/work/gh-aw-mcpg/gh-aw-mcpg/internal/logger/constants.go conf�� go TqgVVKUfb ache/Python/3.12.12/x64/bin/bash--64 user.name (dns block)
  • nonexistent.local
    • Triggering command: /tmp/go-build801673111/b275/launcher.test /tmp/go-build801673111/b275/launcher.test -test.testlogfile=/tmp/go-build801673111/b275/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true g_.a --global .12/x64/as credential.helpe/opt/hostedtoolcache/go/1.25.6/x64/pkg/tool/linux_amd64/compile go (dns block)
  • slow.example.com
    • Triggering command: /tmp/go-build801673111/b275/launcher.test /tmp/go-build801673111/b275/launcher.test -test.testlogfile=/tmp/go-build801673111/b275/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true g_.a --global .12/x64/as credential.helpe/opt/hostedtoolcache/go/1.25.6/x64/pkg/tool/linux_amd64/compile go (dns block)
  • this-host-does-not-exist-12345.com
    • Triggering command: /tmp/go-build801673111/b284/mcp.test /tmp/go-build801673111/b284/mcp.test -test.testlogfile=/tmp/go-build801673111/b284/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true 64/src/runtime/c-p 64/src/crypto/crgithub.com/github/gh-aw-mcpg/internal/difc ache/go/1.25.6/x-lang=go1.25 user.name (dns block)

If you need me to access, download, or install something from one of these locations, you can either:


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Copilot AI and others added 3 commits February 11, 2026 16:03
Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
Copilot AI changed the title [WIP] Collect responses from tools/list calls into tools.json Add tools.json logging for MCP server tool discovery Feb 11, 2026
Copilot AI requested a review from lpcox February 11, 2026 16:11
@lpcox lpcox marked this pull request as ready for review February 11, 2026 16:22
Copilot AI review requested due to automatic review settings February 11, 2026 16:22
@lpcox lpcox merged commit a5d11f2 into main Feb 11, 2026
9 checks passed
@lpcox lpcox deleted the copilot/collect-tools-responses branch February 11, 2026 16:23
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds automatic logging of backend MCP servers’ available tools into tools.json under the gateway log directory, enabling offline tool discovery/auditing.

Changes:

  • Introduces a new ToolsLogger that maintains a per-server tool catalog and persists it to tools.json.
  • Hooks tool discovery logging into unified server backend tool registration and wires logger init/cleanup into CLI lifecycle.
  • Adds unit + integration tests and updates docs (README/AGENTS) to describe the new log artifact.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
internal/logger/tools_logger.go Implements the ToolsLogger and global helpers for writing tools.json.
internal/logger/tools_logger_test.go Unit tests for tools catalog initialization, updates, multi-server behavior, and fallback.
internal/server/unified.go Captures tool metadata from tools/list and forwards it to ToolsLogger.
internal/logger/global_helpers.go Extends global logger generic helpers to include ToolsLogger.
internal/cmd/root.go Initializes and closes the tools logger during CLI startup/shutdown.
test/integration/tools_json_test.go End-to-end test verifying tools.json creation and expected contents.
README.md Documents tools.json as part of the gateway log outputs.
AGENTS.md Adds a “Tools Catalog (tools.json)” section and logging list update.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +44 to +63
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
},
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.
Comment on lines +29 to +45
// Extract port from config if possible, otherwise use default
port := "13120" // Default port
var configMap map[string]interface{}
if err := json.Unmarshal([]byte(jsonConfig), &configMap); err == nil {
if gateway, ok := configMap["gateway"].(map[string]interface{}); ok {
if portNum, ok := gateway["port"].(float64); ok {
port = fmt.Sprintf("%d", int(portNum))
}
}
}

cmd := exec.CommandContext(ctx, binaryPath,
"--config-stdin",
"--listen", "127.0.0.1:"+port,
"--log-dir", logDir,
"--routed",
)
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.

This helper hard-codes a default port (13120) and the test later hard-codes the gateway URL to the same port. This can make the integration test flaky when the port is already in use (especially with local runs or parallel CI jobs). Prefer selecting a free port (e.g., bind to 127.0.0.1:0 to discover an available port) and using that value consistently for both --listen and the health-check URL, or call the existing killProcessOnPort helper before starting the gateway.

Copilot uses AI. Check for mistakes.
Comment on lines +59 to +67
// Start a goroutine to log output if test fails
go func() {
<-ctx.Done()
if t.Failed() {
t.Logf("Gateway STDOUT: %s", stdout.String())
t.Logf("Gateway STDERR: %s", stderr.String())
}
}()

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.

The goroutine logs via t.Logf after waiting on ctx.Done(). If the context is canceled as the test exits, this goroutine can race with test completion and trigger "Log in goroutine after ... has completed" panics. Prefer logging buffered stdout/stderr via a defer/t.Cleanup in the main goroutine (or ensure the goroutine is joined before the test returns).

Suggested change
// Start a goroutine to log output if test fails
go func() {
<-ctx.Done()
if t.Failed() {
t.Logf("Gateway STDOUT: %s", stdout.String())
t.Logf("Gateway STDERR: %s", stderr.String())
}
}()
// Register cleanup to log output if test fails
t.Cleanup(func() {
if t.Failed() {
t.Logf("Gateway STDOUT: %s", stdout.String())
t.Logf("Gateway STDERR: %s", stderr.String())
}
})

Copilot uses AI. Check for mistakes.
Comment on lines +125 to +143
// Give the gateway time to initialize and register tools
time.Sleep(2 * time.Second)

// Check that tools.json was created
toolsPath := filepath.Join(tmpDir, "tools.json")
toolsData, err := os.ReadFile(toolsPath)
require.NoError(err, "tools.json should exist")
t.Logf("✓ tools.json found at %s", toolsPath)

// Parse the tools.json file
var tools struct {
Servers map[string][]struct {
Name string `json:"name"`
Description string `json:"description"`
} `json:"servers"`
}
err = json.Unmarshal(toolsData, &tools)
require.NoError(err, "tools.json should be valid JSON")

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.

Using a fixed time.Sleep(2 * time.Second) to wait for tools.json creation can be flaky on slow CI machines. Prefer polling for tools.json existence + valid JSON (with a timeout) similarly to waitForServer, so the test waits only as long as needed and fails deterministically if the file is never written.

Suggested change
// Give the gateway time to initialize and register tools
time.Sleep(2 * time.Second)
// Check that tools.json was created
toolsPath := filepath.Join(tmpDir, "tools.json")
toolsData, err := os.ReadFile(toolsPath)
require.NoError(err, "tools.json should exist")
t.Logf("✓ tools.json found at %s", toolsPath)
// Parse the tools.json file
var tools struct {
Servers map[string][]struct {
Name string `json:"name"`
Description string `json:"description"`
} `json:"servers"`
}
err = json.Unmarshal(toolsData, &tools)
require.NoError(err, "tools.json should be valid JSON")
// Poll for tools.json to be created and contain valid JSON
toolsPath := filepath.Join(tmpDir, "tools.json")
var tools struct {
Servers map[string][]struct {
Name string `json:"name"`
Description string `json:"description"`
} `json:"servers"`
}
deadline := time.Now().Add(15 * time.Second)
for {
if time.Now().After(deadline) {
t.Fatalf("timed out waiting for tools.json to be created and contain valid JSON at %s", toolsPath)
}
toolsData, err := os.ReadFile(toolsPath)
if err != nil {
// File not yet created; wait a bit and retry
time.Sleep(200 * time.Millisecond)
continue
}
if err := json.Unmarshal(toolsData, &tools); err != nil {
// File exists but is not yet valid JSON; wait and retry
time.Sleep(200 * time.Millisecond)
continue
}
t.Logf("✓ tools.json found at %s", toolsPath)
break
}

Copilot uses AI. Check for mistakes.
@github-actions
Copy link
Contributor

Add tools.json logging for MCP server tool discovery
Add agentic workflow for daily GPL dependency detection
GitHub MCP: ✅
Serena activate_project: ✅
Playwright title: ✅
File writing: ✅
Bash cat: ✅
Overall: PASS

AI generated by Smoke Codex

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments