Summary
Two related bugs in the MCP client framework affect usability when connecting to external MCP servers (e.g., OEM .NET servers). Both exist in the Python implementation; the C++ framework has the naming issue and lacks config loading entirely.
Bug 1: MCP Config Uses Fallback Instead of Stacking
Current Behavior
MCPConfig.__init__() in src/gaia/mcp/client/config.py:27-40 uses a fallback lookup — it picks the first config file found and ignores the other:
local_config = Path.cwd() / "mcp_servers.json"
if local_config.exists():
config_file = local_config # uses local, IGNORES global
else:
config_file = ~/.gaia/mcp_servers.json # falls back to global
Expected Behavior
Config files should stack (merge), with local overriding global on key conflicts:
- Load
~/.gaia/mcp_servers.json (global/user-level servers)
- Load
./mcp_servers.json (project-local servers)
- Merge: local entries override global entries with the same key
Example:
# ~/.gaia/mcp_servers.json (global)
{"mcpServers": {"time": {"command": "uvx", "args": ["mcp-server-time"]}}}
# ./mcp_servers.json (local)
{"mcpServers": {"acer": {"command": "dotnet", "args": ["run", "--project", "/path/to/oem"]}}}
# Result: both "time" and "acer" are available
If both files define the same server name, the local version wins.
Affected Files
- Python:
src/gaia/mcp/client/config.py — MCPConfig.__init__() and _load()
- C++: No config loader exists — needs new
MCPConfig class in cpp/include/gaia/mcp_config.h and cpp/src/mcp_config.cpp, plus Agent::loadMcpServersFromConfig() in cpp/src/agent.cpp
Reproduction
- Clear
~/.gaia/mcp_servers.json (empty {"mcpServers": {}})
- Put server entries in
./mcp_servers.json
- Run an agent with default
MCPClientMixin.__init__(self) — no servers load because the empty global file is found first and the local file is never checked
Bug 2: MCP Tool Display Shows Internal Namespaced Name
Current Behavior
When GAIA registers MCP tools, it namespaces them as mcp_<server>_<tool> for internal registry uniqueness (e.g., mcp_acer_launch_experience_zone). This namespaced name is shown directly to the user in console output:
🔧 Executing operation
Tool: mcp_acer_launch_experience_zone
The [MCP:acer] prefix is also prepended to tool descriptions in the system prompt, making them verbose:
- mcp_acer_launch_experience_zone(): [MCP:acer] launch_experience_zone is an MCP tool designed to...
Expected Behavior
Console output should show the original tool name with the server as context:
🔧 Executing operation
Tool: launch_experience_zone (acer)
The metadata to do this already exists — _mcp_tool_name and _mcp_server are stored in the tool registry entry (src/gaia/mcp/client/mcp_client.py:58-59), but print_tool_usage() in console.py doesn't use them.
Affected Files
Python:
src/gaia/agents/base/console.py:794 — print_tool_usage() displays tool_name directly
src/gaia/agents/base/tools.py — Add utility to resolve display-friendly name from registry metadata
C++:
cpp/src/agent.cpp:523,525 — printToolUsage(toolName) passes the mangled name
cpp/include/gaia/types.h — ToolInfo already has mcpServer and mcpToolName fields (set in mcp_client.cpp:33-34)
cpp/src/console.cpp or cpp/include/gaia/console.h — printToolUsage needs display-friendly name resolution
Implementation Notes
Config Stacking Logic (identical for Python and C++)
if config_file explicitly provided:
load only that file (preserves existing behavior for direct callers)
else:
global_servers = load(~/.gaia/mcp_servers.json) or {}
local_servers = load(./mcp_servers.json) or {}
merged = {**global_servers, **local_servers} # local wins on conflict
save target = local file if it existed, else global
Tool Display Name Resolution
def get_tool_display_name(tool_name: str) -> str:
tool = _TOOL_REGISTRY.get(tool_name)
if tool and "_mcp_tool_name" in tool:
return f"{tool['_mcp_tool_name']} ({tool['_mcp_server']})"
return tool_name
Acceptance Criteria
AC1: Config Stacking — Python
AC2: Config Stacking — C++
AC3: Tool Display — Python
AC4: Tool Display — C++
AC5: Regression — No Breaking Changes
AC6: Tests
AC7: Documentation
Test Matrix
| Scenario |
Expected |
| Global only has servers |
All global servers load |
| Local only has servers |
All local servers load |
| Both have servers (no overlap) |
All servers from both load |
| Both have same key |
Local version used |
| Neither file exists |
Empty config, no errors |
Explicit config_file passed |
Only that file loaded (no stacking) |
| Tool display for MCP tool |
Shows original_name (server) |
| Tool display for native tool |
Shows tool name unchanged |
Existing Test Files to Update
tests/unit/mcp/client/test_mcp_client_manager.py — TestMCPConfig class
tests/unit/mcp/client/test_mcp_client_mixin.py — tool registration tests
cpp/tests/test_mcp_client.cpp — C++ tool schema tests
Documentation to Update
docs/sdk/infrastructure/mcp.mdx — Config section
docs/sdk/sdks/mcp.mdx — If it covers config
Summary
Two related bugs in the MCP client framework affect usability when connecting to external MCP servers (e.g., OEM .NET servers). Both exist in the Python implementation; the C++ framework has the naming issue and lacks config loading entirely.
Bug 1: MCP Config Uses Fallback Instead of Stacking
Current Behavior
MCPConfig.__init__()insrc/gaia/mcp/client/config.py:27-40uses a fallback lookup — it picks the first config file found and ignores the other:Expected Behavior
Config files should stack (merge), with local overriding global on key conflicts:
~/.gaia/mcp_servers.json(global/user-level servers)./mcp_servers.json(project-local servers)Example:
If both files define the same server name, the local version wins.
Affected Files
src/gaia/mcp/client/config.py—MCPConfig.__init__()and_load()MCPConfigclass incpp/include/gaia/mcp_config.handcpp/src/mcp_config.cpp, plusAgent::loadMcpServersFromConfig()incpp/src/agent.cppReproduction
~/.gaia/mcp_servers.json(empty{"mcpServers": {}})./mcp_servers.jsonMCPClientMixin.__init__(self)— no servers load because the empty global file is found first and the local file is never checkedBug 2: MCP Tool Display Shows Internal Namespaced Name
Current Behavior
When GAIA registers MCP tools, it namespaces them as
mcp_<server>_<tool>for internal registry uniqueness (e.g.,mcp_acer_launch_experience_zone). This namespaced name is shown directly to the user in console output:The
[MCP:acer]prefix is also prepended to tool descriptions in the system prompt, making them verbose:Expected Behavior
Console output should show the original tool name with the server as context:
The metadata to do this already exists —
_mcp_tool_nameand_mcp_serverare stored in the tool registry entry (src/gaia/mcp/client/mcp_client.py:58-59), butprint_tool_usage()inconsole.pydoesn't use them.Affected Files
Python:
src/gaia/agents/base/console.py:794—print_tool_usage()displaystool_namedirectlysrc/gaia/agents/base/tools.py— Add utility to resolve display-friendly name from registry metadataC++:
cpp/src/agent.cpp:523,525—printToolUsage(toolName)passes the mangled namecpp/include/gaia/types.h—ToolInfoalready hasmcpServerandmcpToolNamefields (set inmcp_client.cpp:33-34)cpp/src/console.cpporcpp/include/gaia/console.h—printToolUsageneeds display-friendly name resolutionImplementation Notes
Config Stacking Logic (identical for Python and C++)
Tool Display Name Resolution
Acceptance Criteria
AC1: Config Stacking — Python
~/.gaia/mcp_servers.jsonexists, all servers from it are loaded./mcp_servers.jsonexists, all servers from it are loaded./) version is usedconfig_fileis explicitly passed toMCPConfig(), only that file is loaded (no stacking) — preserves existing behavior for direct callers likegaia mcp add_save()writes to the local config file if it existed during load, otherwise to the global file"servers"key is still supported in both global and local filesAC2: Config Stacking — C++
MCPConfigclass exists incpp/include/gaia/mcp_config.handcpp/src/mcp_config.cppMCPConfigimplements the same stacking logic as Python (global + local merge, local overrides)Agent::loadMcpServersFromConfig()loads servers fromMCPConfigand callsconnectMcpServer()for eachAC3: Tool Display — Python
launch_experience_zone (acer)format instead ofmcp_acer_launch_experience_zoneprint_tool_usage()(e.g.,query_documents→ "Searching through indexed documents...") continue to work unchanged_mcp_tool_nameand_mcp_servermetadata fields remain in the tool registry entry for routing and debuggingAC4: Tool Display — C++
original_name (server)formatAC5: Regression — No Breaking Changes
MCPConfig(config_file="/explicit/path.json")still loads only that file — no stacking, no change in behaviorMCPClientMixin.__init__(self, auto_load_config=False)still skips config loadingMCPClientMixin.__init__(self, config_file="path")still uses only the specified filegaia mcp add <server>still writes to the correct config fileconnect_mcp_server()inline (without config files) continue to workmcp_<server>_<tool>as the internal key — only display changes_resolve_tool_name()) still handles LLM calling tools with or without themcp_prefix_unregister_mcp_tools()still correctly removes tools by namespaced keyAC6: Tests
AC7: Documentation
docs/sdk/infrastructure/mcp.mdxdocuments config stacking behaviorTest Matrix
config_filepassedoriginal_name (server)Existing Test Files to Update
tests/unit/mcp/client/test_mcp_client_manager.py—TestMCPConfigclasstests/unit/mcp/client/test_mcp_client_mixin.py— tool registration testscpp/tests/test_mcp_client.cpp— C++ tool schema testsDocumentation to Update
docs/sdk/infrastructure/mcp.mdx— Config sectiondocs/sdk/sdks/mcp.mdx— If it covers config