#22996·llama_index

[Bug]: MCP tool_call_logs_callback can receive empty logs when application logging is already configured

Author: anupamking01Created Sep 8, 2026Updated Sep 15, 2026

Bug Description

BasicMCPClient._configure_tool_call_logs_callback() currently relies on logging.basicConfig(..., handlers=[stream_handler]) to attach a temporary StringIO handler before an MCP tool call.

logging.basicConfig() is intentionally a no-op once the root logger already has handlers unless force=True is supplied. In a typical FastAPI/Jupyter/CLI application where logging has already been configured, the newly created stream_handler is therefore not attached to the root logger, but call_tool() still reads from that handler and forwards its contents to tool_call_logs_callback.

Current flow in llama-index-tools-mcp/llama_index/tools/mcp/client.py:

def _configure_tool_call_logs_callback(self) -> io.StringIO:
    handler = io.StringIO()
    stream_handler = logging.StreamHandler(handler)
    logging.basicConfig(
        level=logging.DEBUG,
        format="%(asctime)s - %(name)s - %(levelname)s - %(message)s\n",
        handlers=[stream_handler],
    )
    logging.getLogger("mcp").setLevel(logging.DEBUG)
    logging.getLogger("httpx").setLevel(logging.DEBUG)
    return handler

and later:

handler = self._configure_tool_call_logs_callback()
...
extra_values = handler.getvalue().split("\n")
await self.tool_call_logs_callback(extra_values)

If logging was configured earlier, handler.getvalue() can remain empty even though MCP/httpx log records were emitted.

Minimal Reproduction

A focused unit test does not need a live MCP server to demonstrate the handler problem:

import logging

# Simulate an application that configured logging before constructing BasicMCPClient.
logging.basicConfig(level=logging.INFO, force=True)

client = BasicMCPClient(...)
stream = client._configure_tool_call_logs_callback()

logging.getLogger("mcp").debug("mcp-debug-message")

assert "mcp-debug-message" in stream.getvalue()  # currently can fail

The failure occurs because the temporary StreamHandler(stream) is not installed after the first basicConfig() call.

Additional Side Effect

The helper also permanently changes the mcp and httpx logger levels to DEBUG and does not restore them after the tool call. A single client configured with tool_call_logs_callback can therefore alter logging verbosity for the entire host application.

Expected Behavior

Enabling tool_call_logs_callback should:

  1. reliably capture logs for that tool call regardless of prior application logging configuration;
  2. avoid mutating global/root logging configuration beyond the scope of the capture;
  3. detach/restore any temporary handlers and log levels after the call.

Suggested Direction

Instead of logging.basicConfig(), temporarily attach a dedicated handler directly to the relevant logger(s), for example mcp and httpx, and restore their prior levels in a finally block.

Conceptually:

stream = io.StringIO()
handler = logging.StreamHandler(stream)
logger = logging.getLogger("mcp")
old_level = logger.level
logger.addHandler(handler)
logger.setLevel(logging.DEBUG)
try:
    ...
finally:
    logger.removeHandler(handler)
    logger.setLevel(old_level)

The implementation may need to cover both mcp and httpx, but the important part is avoiding basicConfig() for per-call capture.

I searched the tracker for tool_call_logs_callback, basicConfig, and MCP log capture and did not find an existing report for this behavior.

AI-assisted review disclosure: an AI coding assistant was used to inspect the current logging/callback path and help draft this report. The basicConfig() behavior is standard-library behavior and the issue is scoped to the existing callback implementation.