Skip to content
Open
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
10 changes: 9 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,12 @@ __pycache__/
frontend/env.local
frontend/node_modules/

repomix-*
repomix-*

# PID files for frontend and backend
*.pid

# Local logs
logs
logs/*
debug_logs/
4 changes: 2 additions & 2 deletions core/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ out/
Thumbs.db

# Log files
logs/
logs/*
*.log

# Temporary & Cache files
Expand Down Expand Up @@ -100,4 +100,4 @@ bug_commit.txt
!/runs/.gitignore
!/.github

.claude
.claude
172 changes: 153 additions & 19 deletions core/agent_core/config/app_config.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,38 @@
import os
import json
import logging
import threading
import copy
from types import MappingProxyType
from typing import Dict, Any

logger = logging.getLogger(__name__)

def load_native_mcp_config():
# Thread-safe MCP configuration loader
# Uses a lock to ensure atomic loading and immutable proxy for read access
_mcp_config_lock = threading.Lock()
_mcp_server_categories_internal: Dict[str, Any] = {}
_native_mcp_servers_internal: Dict[str, Any] = {}
_mcp_config_loaded = False


def _load_mcp_config_internal() -> tuple[Dict[str, Any], Dict[str, Any]]:
"""
Loads native MCP server configurations from environment variables or a configuration file.

Returns:
dict: A dictionary of enabled MCP server configurations.
Internal function to load MCP configuration.
Returns tuple of (enabled_servers, categories).

IMPORTANT: Category-based toolset matching is STRICT:
- Servers WITHOUT an explicit 'category' field default to 'uncategorized'
- 'uncategorized' servers are NOT matched by 'all_user_specified_mcp_servers' or 'all_google_related_mcp_servers'
- To include a server in a category toolset, you MUST explicitly set its 'category' field

Standard categories:
- "google_related": Google/Gemini ecosystem servers
- "user_specified": User-added domain-specific servers
- "uncategorized": Default for servers without explicit category (NOT matched by category toolsets)
"""
config_str = os.getenv("NATIVE_MCP_SERVERS_CONFIG")
config_path = os.getenv("NATIVE_MCP_SERVERS_CONFIG_PATH", "mcp.json") # Add default value
config_path = os.getenv("NATIVE_MCP_SERVERS_CONFIG_PATH", "mcp.json")
config = {}

if config_path and os.path.exists(config_path):
Expand All @@ -29,21 +49,135 @@ def load_native_mcp_config():
except json.JSONDecodeError as e:
logger.error("native_mcp_config_env_parse_failed", extra={"error": str(e)})

# Return only enabled server configurations
# Build category mapping and return only enabled server configurations
enabled_servers = {}
categories = {}

if isinstance(config, dict) and "mcpServers" in config:
for name, server_conf in config["mcpServers"].items():
if isinstance(server_conf, dict) and server_conf.get("enabled", False):
# [MODIFIED] Only require the transport field to be present
if "transport" in server_conf:
enabled_servers[name] = server_conf
else:
for name, server_conf in config["mcpServers"].items():
if not isinstance(server_conf, dict):
continue

# STRICT category handling: default to 'uncategorized' if not specified
# 'uncategorized' servers will NOT be matched by category-based toolsets
explicit_category = server_conf.get("category")
category = explicit_category if explicit_category else "uncategorized"

if not explicit_category:
logger.warning("mcp_server_missing_category", extra={
"server_name": name,
"hint": "Server has no 'category' field. It will NOT be included in category-based toolsets like 'all_user_specified_mcp_servers'. Add 'category': 'user_specified' to include it."
})

categories[name] = {
"category": category,
"enabled": server_conf.get("enabled", False),
"has_explicit_category": explicit_category is not None
}

if server_conf.get("enabled", False):
if "transport" in server_conf:
enabled_servers[name] = server_conf
else:
logger.warning("native_mcp_server_config_incomplete", extra={"server_name": name, "missing_field": "transport"})
else:
logger.debug("native_mcp_server_disabled_or_invalid", extra={"server_name": name})
else:
logger.debug("native_mcp_server_disabled_or_invalid", extra={"server_name": name})

logger.info("native_mcp_servers_loaded", extra={"enabled_count": len(enabled_servers), "categories": categories})
return enabled_servers, categories


def _ensure_mcp_config_loaded() -> None:
"""
Thread-safe initialization of MCP configuration.
Uses double-checked locking pattern for efficiency.
"""
global _mcp_config_loaded, _mcp_server_categories_internal, _native_mcp_servers_internal

if _mcp_config_loaded:
return

with _mcp_config_lock:
# Double-check after acquiring lock
if _mcp_config_loaded:
return

servers, categories = _load_mcp_config_internal()
_native_mcp_servers_internal = servers
_mcp_server_categories_internal = categories
_mcp_config_loaded = True


def get_mcp_server_categories() -> MappingProxyType:
"""
Returns an immutable view of MCP server categories.
Thread-safe and prevents accidental modification.
"""
_ensure_mcp_config_loaded()
return MappingProxyType(_mcp_server_categories_internal)


def get_native_mcp_servers() -> Dict[str, Any]:
"""
Returns the enabled MCP server configurations.
Thread-safe initialization on first access.

Returns a deep copy to prevent callers from mutating the internal state,
since MCP server configs contain nested dicts (transport params, etc.).
"""
_ensure_mcp_config_loaded()
return copy.deepcopy(_native_mcp_servers_internal)


def reload_mcp_config() -> None:
"""
Force reload of MCP configuration.
Thread-safe - acquires lock before reloading.
Use sparingly, typically only for hot-reload scenarios.
"""
global _mcp_config_loaded, _mcp_server_categories_internal, _native_mcp_servers_internal

with _mcp_config_lock:
servers, categories = _load_mcp_config_internal()
_native_mcp_servers_internal = servers
_mcp_server_categories_internal = categories
_mcp_config_loaded = True
logger.info("mcp_config_reloaded")


# Backward compatibility: Module-level access via property-like pattern
# These trigger lazy initialization on first access
class _MCPConfigProxy:
"""Proxy class to provide backward-compatible module-level attribute access."""

@property
def MCP_SERVER_CATEGORIES(self) -> MappingProxyType:
return get_mcp_server_categories()

@property
def NATIVE_MCP_SERVERS(self) -> Dict[str, Any]:
return get_native_mcp_servers()


# For backward compatibility, expose as module-level variables
# Note: These are now lazy-loaded and thread-safe
def _get_mcp_server_categories():
"""Backward-compatible accessor for MCP_SERVER_CATEGORIES."""
return get_mcp_server_categories()


def _get_native_mcp_servers():
"""Backward-compatible accessor for NATIVE_MCP_SERVERS."""
return get_native_mcp_servers()


logger.info("native_mcp_servers_loaded", extra={"enabled_count": len(enabled_servers)})
return enabled_servers
# Trigger initial load for backward compatibility
# This ensures the config is loaded at module import time as before
_ensure_mcp_config_loaded()

# Global configuration
NATIVE_MCP_SERVERS = load_native_mcp_config()
# Expose read-only views for backward compatibility
# WARNING: These are snapshots at import time. Use get_*() functions for guaranteed fresh access.
# MCP_SERVER_CATEGORIES uses MappingProxyType to prevent mutation
# NATIVE_MCP_SERVERS returns a deepcopy to prevent mutation of nested dicts
MCP_SERVER_CATEGORIES = MappingProxyType(_mcp_server_categories_internal)
NATIVE_MCP_SERVERS = copy.deepcopy(_native_mcp_servers_internal)
1 change: 0 additions & 1 deletion core/agent_core/config/logging_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import logging
import sys
import os
from pythonjsonlogger import jsonlogger
from contextvars import ContextVar, copy_context
import asyncio

Expand Down
5 changes: 3 additions & 2 deletions core/agent_core/events/event_strategies.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ def __init__(self, ingestor_func: Callable, default_injection_mode: str, default
tagged_content_ingestor,
observer_failure_ingestor,
user_prompt_ingestor,
protocol_aware_ingestor
protocol_aware_ingestor,
principal_completed_ingestor
)

EVENT_STRATEGY_REGISTRY: Dict[str, EventHandlingStrategy] = {
Expand Down Expand Up @@ -59,7 +60,7 @@ def __init__(self, ingestor_func: Callable, default_injection_mode: str, default
}
),
"PRINCIPAL_COMPLETED": EventHandlingStrategy(
ingestor_func=generic_message_ingestor,
ingestor_func=principal_completed_ingestor,
default_injection_mode="append_as_new_message",
default_params={"role": "user", "is_persistent_in_memory": True}
),
Expand Down
Loading