diff --git a/src/cli.py b/src/cli.py index 4ee2e04..a8a613c 100644 --- a/src/cli.py +++ b/src/cli.py @@ -5,7 +5,7 @@ import argparse from src import main -from src.config.server_config import McpServerConfig, init_server_config +from src.config.server_config import init_server_config def parse_args(): @@ -27,16 +27,19 @@ def cli_main(): """ # Parse command-line arguments args = parse_args() + + # Initialize server config with CLI arguments - this will also update the logger init_server_config( - McpServerConfig( - port_client_id=args.client_id, - port_client_secret=args.client_secret, - region=args.region, - log_level=args.log_level, - api_validation_enabled=args.api_validation_enabled.lower() == "true", - ).model_dump() + { + "port_client_id": args.client_id, + "port_client_secret": args.client_secret, + "region": args.region, + "log_level": args.log_level, + "api_validation_enabled": args.api_validation_enabled.lower() == "true", + } ) - # Call the main function with command-line arguments + + # Call the main function main() diff --git a/src/client/actions.py b/src/client/actions.py index b1e45a2..e17f84e 100644 --- a/src/client/actions.py +++ b/src/client/actions.py @@ -3,7 +3,7 @@ from pyport import PortClient -from src.config import config +from src.config import get_config from src.models.actions import Action from src.utils import logger @@ -52,7 +52,7 @@ async def get_all_actions(self, trigger_type: str = "self-service") -> list[Acti else: logger.debug(f"User lacks permission for action: {action_identifier}") - if config.api_validation_enabled: + if get_config().api_validation_enabled: logger.debug("Validating actions") return [Action(**action) for action in filtered_actions] else: @@ -65,7 +65,7 @@ async def get_action(self, action_identifier: str) -> Action: response = self._client.make_request("GET", f"actions/{action_identifier}") result = response.json().get("action") - if config.api_validation_enabled: + if get_config().api_validation_enabled: logger.debug("Validating action") return Action(**result) else: @@ -88,7 +88,7 @@ async def create_action(self, action_data: dict[str, Any]) -> Action: result = result.get("action", {}) - if config.api_validation_enabled: + if get_config().api_validation_enabled: logger.debug("Validating action") action = Action(**result) else: @@ -114,7 +114,7 @@ async def update_action(self, action_identifier: str, action_data: dict[str, Any logger.info(f"Action '{action_identifier}' updated in Port") result = result.get("action", {}) - if config.api_validation_enabled: + if get_config().api_validation_enabled: logger.debug("Validating action") action = Action(**result) else: diff --git a/src/client/agent.py b/src/client/agent.py index 1e034a3..0184688 100644 --- a/src/client/agent.py +++ b/src/client/agent.py @@ -3,7 +3,7 @@ from pyport import PortClient -from src.config import config +from src.config import get_config from src.models.agent.port_agent_response import PortAgentResponse, PortAgentTriggerResponse from src.utils import logger from src.utils.errors import PortError @@ -59,7 +59,7 @@ async def get_invocation_status(self, identifier: str) -> PortAgentResponse: if urls: action_url = urls[0] - if config.api_validation_enabled: + if get_config().api_validation_enabled: return PortAgentResponse( identifier=identifier, status=status, diff --git a/src/client/blueprints.py b/src/client/blueprints.py index 69a742f..a728e6c 100644 --- a/src/client/blueprints.py +++ b/src/client/blueprints.py @@ -3,7 +3,7 @@ from pyport import PortClient -from src.config import config +from src.config import get_config from src.models.blueprints import Blueprint from src.utils import logger from src.utils.errors import PortError @@ -23,7 +23,7 @@ async def get_blueprints(self) -> list[Blueprint]: logger.info("Got blueprints from Port") logger.debug(f"Response for get blueprints: {blueprints}") - if config.api_validation_enabled: + if get_config().api_validation_enabled: logger.debug("Validating blueprints") return [Blueprint(**bp) for bp in blueprints] else: @@ -39,7 +39,7 @@ async def get_blueprint(self, blueprint_identifier: str) -> Blueprint: logger.info(f"Got blueprint '{blueprint_identifier}' from Port") - if config.api_validation_enabled: + if get_config().api_validation_enabled: logger.debug("Validating blueprint") return Blueprint(**bp_data) else: @@ -62,7 +62,7 @@ async def create_blueprint(self, blueprint_data: dict[str, Any]) -> Blueprint: result = result.get("blueprint", {}) - if config.api_validation_enabled: + if get_config().api_validation_enabled: logger.debug("Validating blueprint") blueprint = Blueprint(**result) else: @@ -86,7 +86,7 @@ async def update_blueprint(self, blueprint_data: dict[str, Any]) -> Blueprint: logger.info("Blueprint updated in Port") result = result.get("blueprint", {}) - if config.api_validation_enabled: + if get_config().api_validation_enabled: logger.debug("Validating blueprint") blueprint = Blueprint(**result) else: diff --git a/src/client/client.py b/src/client/client.py index 2bd3b8f..380720f 100644 --- a/src/client/client.py +++ b/src/client/client.py @@ -11,7 +11,7 @@ from src.client.entities import PortEntityClient from src.client.permissions import PortPermissionsClient from src.client.scorecards import PortScorecardClient -from src.config import config +from src.config import get_config from src.models.action_run.action_run import ActionRun from src.models.actions.action import Action from src.models.agent import PortAgentResponse @@ -31,11 +31,15 @@ def __init__( client_id: str | None = None, client_secret: str | None = None, region: str = "EU", - base_url: str = config.port_api_base, + base_url: str | None = None, ): if not client_id or not client_secret: logger.warning("PortClient initialized without credentials") + # Set default base_url if not provided + if base_url is None: + base_url = get_config().port_api_base + self.base_url = base_url self.client_id = client_id self.client_secret = client_secret diff --git a/src/client/entities.py b/src/client/entities.py index 3b4eae2..0f7ee07 100644 --- a/src/client/entities.py +++ b/src/client/entities.py @@ -2,7 +2,7 @@ from pyport import PortClient -from src.config import config +from src.config import get_config from src.models.entities import EntityResult from src.utils import PortError, logger @@ -22,7 +22,7 @@ async def get_entities(self, blueprint_identifier: str) -> list[EntityResult]: logger.info(f"Got {len(entities_data)} entities for blueprint '{blueprint_identifier}' from Port") logger.debug(f"Response for get entities: {entities_data}") - if config.api_validation_enabled: + if get_config().api_validation_enabled: logger.debug("Validating entities") return [EntityResult(**entity_data) for entity_data in entities_data] else: @@ -37,7 +37,7 @@ async def search_entities(self, blueprint_identifier: str, search_query: dict[st logger.info(f"Got {len(entities_data)} entities for blueprint '{blueprint_identifier}' from Port") logger.debug(f"Response for search entities: {entities_data}") - if config.api_validation_enabled: + if get_config().api_validation_enabled: logger.debug("Validating entities") return [EntityResult(**entity_data) for entity_data in entities_data] else: @@ -51,7 +51,7 @@ async def get_entity(self, blueprint_identifier: str, entity_identifier: str) -> logger.info(f"Got entity '{entity_identifier}' from blueprint '{blueprint_identifier}' from Port") logger.debug(f"Response for get entity: {entity_data}") - if config.api_validation_enabled: + if get_config().api_validation_enabled: logger.debug("Validating entity") return EntityResult(**entity_data) else: @@ -83,7 +83,7 @@ async def create_entity(self, blueprint_identifier: str, entity_data: dict[str, entity = created_data.get("entity", {}) logger.debug(f"Response for create entity: {entity}") - if config.api_validation_enabled: + if get_config().api_validation_enabled: logger.debug("Validating entity") return EntityResult(**entity) else: @@ -105,7 +105,7 @@ async def update_entity(self, blueprint_identifier: str, entity_identifier: str, entity = updated_data.get("entity", {}) logger.debug(f"Response for update entity: {entity}") - if config.api_validation_enabled: + if get_config().api_validation_enabled: logger.debug("Validating entity") return EntityResult(**entity) else: diff --git a/src/client/scorecards.py b/src/client/scorecards.py index f415f71..f42bee3 100644 --- a/src/client/scorecards.py +++ b/src/client/scorecards.py @@ -3,7 +3,7 @@ from pyport import PortClient -from src.config import config +from src.config import get_config from src.models.scorecards import Scorecard from src.utils import logger from src.utils.errors import PortError @@ -23,7 +23,7 @@ async def get_scorecards(self, blueprint_identifier: str) -> list[Scorecard]: logger.info(f"Got {len(scorecards_data)} scorecards for blueprint '{blueprint_identifier}' from Port") logger.debug(f"Response for get scorecards: {scorecards_data}") - if config.api_validation_enabled: + if get_config().api_validation_enabled: logger.debug("Validating scorecards") return [Scorecard(**scorecard_data) for scorecard_data in scorecards_data] else: @@ -65,7 +65,7 @@ async def create_scorecard(self, blueprint_id: str, scorecard_data: dict[str, An logger.debug(f"Response for create scorecard: {data}") - if config.api_validation_enabled: + if get_config().api_validation_enabled: logger.debug("Validating scorecard") return Scorecard(**data) else: @@ -117,7 +117,7 @@ async def update_scorecard(self, blueprint_id: str, scorecard_id: str, scorecard logger.debug(f"Response for update scorecard: {data}") - if config.api_validation_enabled: + if get_config().api_validation_enabled: logger.debug("Validating scorecard") return Scorecard(**data) else: diff --git a/src/config/__init__.py b/src/config/__init__.py index 4027ddc..4f57ed6 100644 --- a/src/config/__init__.py +++ b/src/config/__init__.py @@ -1,5 +1,5 @@ """Configuration package for Port.io MCP server.""" -from .server_config import McpServerConfig, config, init_server_config +from .server_config import McpServerConfig, get_config, init_server_config -__all__ = ["McpServerConfig", "init_server_config", "config"] +__all__ = ["McpServerConfig", "init_server_config", "get_config"] diff --git a/src/config/server_config.py b/src/config/server_config.py index d6f6e67..f7e142c 100644 --- a/src/config/server_config.py +++ b/src/config/server_config.py @@ -5,7 +5,7 @@ from dotenv import load_dotenv from pydantic import BaseModel, Field, ValidationError -from src.utils import PortError, logger +from src.utils import PortError # Load environment variables from .env file if it exists, but don't override existing env vars load_dotenv(override=False) @@ -56,6 +56,9 @@ def init_server_config(override: dict[str, Any] | None = None): log_level=override.get("log_level", "ERROR"), api_validation_enabled=override.get("api_validation_enabled", "false") == "true", ) + # Update logger with new config + from src.utils.logger import update_logger_with_config + update_logger_with_config(config) return config try: client_id = os.environ.get("PORT_CLIENT_ID", "") @@ -72,11 +75,24 @@ def init_server_config(override: dict[str, Any] | None = None): log_level=cast(Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], log_level), api_validation_enabled=api_validation_enabled, ) + # Update logger with new config + from src.utils.logger import update_logger_with_config + update_logger_with_config(config) return config except ValidationError as e: message = f"❌ Error initializing server config: {e.errors()}" + # Import logger here to avoid circular dependency + from src.utils import logger logger.error(message) raise PortError(message) from e -config: McpServerConfig = init_server_config() +config: McpServerConfig | None = None + + +def get_config() -> McpServerConfig: + """Get the global config, initializing it if necessary.""" + global config + if config is None: + config = init_server_config() + return config diff --git a/src/maps/__init__.py b/src/maps/__init__.py index 8bc4cdd..adc476e 100644 --- a/src/maps/__init__.py +++ b/src/maps/__init__.py @@ -1,5 +1,5 @@ """AI Agent related data models for Port.io.""" -from .tool_map import tool_map +from .tool_map import get_tool_map -__all__ = ["tool_map"] +__all__ = ["get_tool_map"] diff --git a/src/maps/tool_map.py b/src/maps/tool_map.py index 432e6ec..3383434 100644 --- a/src/maps/tool_map.py +++ b/src/maps/tool_map.py @@ -1,19 +1,34 @@ from src.client import PortClient -from src.config import config +from src.config import get_config from src.models.tools import ToolMap from src.utils import logger def init_tool_map() -> ToolMap: + config = get_config() + logger.info(f"Initializing tool map with config region: {config.region}") + logger.debug(f"Client ID available: {bool(config.port_client_id)}") + logger.debug(f"Client secret available: {bool(config.port_client_secret)}") + port_client = PortClient( client_id=config.port_client_id, client_secret=config.port_client_secret, region=config.region, ) + logger.info("PortClient created successfully") + tool_map = ToolMap(port_client=port_client) - logger.info("Initialized tool map") + logger.info("Tool map created, initializing tools...") logger.debug(f"Tool map: {tool_map}") return tool_map -tool_map: ToolMap = init_tool_map() +tool_map: ToolMap | None = None + + +def get_tool_map() -> ToolMap: + """Get the global tool map, initializing it if necessary.""" + global tool_map + if tool_map is None: + tool_map = init_tool_map() + return tool_map diff --git a/src/models/tools/tool_map.py b/src/models/tools/tool_map.py index b3031fe..fe364e2 100644 --- a/src/models/tools/tool_map.py +++ b/src/models/tools/tool_map.py @@ -13,6 +13,7 @@ class ToolMap: port_client: PortClient tools: dict[str, Tool] = field(default_factory=dict) + _dynamic_tools_loaded: bool = field(default=False, init=False) def __post_init__(self): # Register static tools @@ -20,20 +21,48 @@ def __post_init__(self): module = mcp_tools.__dict__[tool] self.register_tool(module(self.port_client)) logger.info(f"ToolMap initialized with {len(self.tools)} static tools") - self._register_dynamic_action_tools() + # Don't load dynamic tools here - they'll be loaded lazily when needed + + async def ensure_dynamic_tools_loaded(self) -> None: + """Ensure dynamic action tools are loaded (async version).""" + if self._dynamic_tools_loaded: + return + + try: + logger.info("Loading dynamic action tools asynchronously...") + dynamic_manager = DynamicActionToolsManager(self.port_client) + dynamic_tools = await dynamic_manager.get_dynamic_action_tools() + + for tool in dynamic_tools: + self.register_tool(tool) + + logger.info(f"Successfully registered {len(dynamic_tools)} dynamic action tools") + if len(dynamic_tools) == 0: + logger.warning("No dynamic action tools were registered - this may indicate a configuration or network issue") + + self._dynamic_tools_loaded = True + except Exception as e: + logger.error(f"Failed to register dynamic action tools: {e}") + logger.exception("Full traceback for dynamic action tools registration failure:") + # Mark as loaded even if failed to avoid repeated attempts + self._dynamic_tools_loaded = True def _register_dynamic_action_tools(self) -> None: - """Register dynamic tools for each Port action.""" + """Register dynamic tools for each Port action (legacy sync version).""" try: + logger.info("Starting dynamic action tools registration...") dynamic_manager = DynamicActionToolsManager(self.port_client) dynamic_tools = dynamic_manager.get_dynamic_action_tools_sync() for tool in dynamic_tools: self.register_tool(tool) - logger.info(f"Registered {len(dynamic_tools)} dynamic action tools") + logger.info(f"Successfully registered {len(dynamic_tools)} dynamic action tools") + if len(dynamic_tools) == 0: + logger.warning("No dynamic action tools were registered - this may indicate a configuration or network issue") except Exception as e: logger.error(f"Failed to register dynamic action tools: {e}") + logger.exception("Full traceback for dynamic action tools registration failure:") def list_tools(self) -> list[types.Tool]: return [ diff --git a/src/server.py b/src/server.py index 2de0db3..c4387c2 100644 --- a/src/server.py +++ b/src/server.py @@ -8,9 +8,9 @@ from mcp.server.lowlevel import Server from src.handlers import execute_tool -from src.maps.tool_map import tool_map +from src.maps.tool_map import get_tool_map from src.utils import logger -from src.config import config +from src.config import get_config def main(): @@ -18,6 +18,7 @@ def main(): # Set logging level based on debug flag logger.info("Starting Port MCP server...") + config = get_config() logger.debug(f"Server config: {config}") # Initialize Port.io client @@ -26,12 +27,18 @@ def main(): @mcp.call_tool() async def call_tool(tool_name: str, arguments: dict[str, Any]): + tool_map = get_tool_map() + # Ensure dynamic tools are loaded before calling + await tool_map.ensure_dynamic_tools_loaded() tool = tool_map.get_tool(tool_name) logger.debug(f"Calling tool: {tool_name} with arguments: {arguments}") return await execute_tool(tool, arguments) @mcp.list_tools() async def list_tools() -> list[types.Tool]: + tool_map = get_tool_map() + # Ensure dynamic tools are loaded before listing + await tool_map.ensure_dynamic_tools_loaded() return tool_map.list_tools() # Run the server diff --git a/src/tools/action/dynamic_actions.py b/src/tools/action/dynamic_actions.py index fe6419a..d869626 100644 --- a/src/tools/action/dynamic_actions.py +++ b/src/tools/action/dynamic_actions.py @@ -143,5 +143,21 @@ async def get_dynamic_action_tools(self) -> list[Tool]: return tools def get_dynamic_action_tools_sync(self) -> list[Tool]: - """Synchronous wrapper for getting dynamic action tools.""" - return asyncio.run(self.get_dynamic_action_tools()) + """Synchronous wrapper for getting dynamic action tools (fallback only).""" + try: + # Check if we're already in an event loop + try: + asyncio.get_running_loop() + # We're in an event loop, this shouldn't happen with the new architecture + logger.warning("get_dynamic_action_tools_sync called from async context - this indicates a design issue") + logger.warning("Dynamic actions should be loaded via ensure_dynamic_tools_loaded() instead") + return [] + except RuntimeError: + # No event loop running, use asyncio.run + logger.info("No event loop running, using asyncio.run for dynamic actions") + return asyncio.run(self.get_dynamic_action_tools()) + + except Exception as e: + logger.error(f"Failed to run dynamic action tools sync: {e}") + logger.exception("Full traceback for dynamic action tools sync failure:") + return [] diff --git a/src/utils/logger.py b/src/utils/logger.py index 36c494c..e2eb683 100644 --- a/src/utils/logger.py +++ b/src/utils/logger.py @@ -6,13 +6,28 @@ import loguru -from src.config import config - -def setup_logging(): +def setup_basic_logging(): + """Set up basic logging without config dependency.""" # Remove default logger loguru.logger.remove() - # Add stdout handler + # Add stdout handler with basic settings + loguru.logger.add( + sys.stdout, + format=""" +{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | + {name}:{function}:{line} - {message}""", + level="INFO", + colorize=True, + ) + return loguru.logger + + +def setup_logging_with_config(config): + """Set up logging with config-specific settings.""" + # Remove existing handlers + loguru.logger.remove() + # Add handler with config settings loguru.logger.add( config.log_path if config.log_path else sys.stdout, format=""" @@ -26,4 +41,16 @@ def setup_logging(): return loguru.logger -logger: loguru.Logger = setup_logging() +# Initialize basic logger immediately to avoid circular dependency +logger: loguru.Logger = setup_basic_logging() + + +def get_logger() -> loguru.Logger: + """Get the global logger.""" + return logger + + +def update_logger_with_config(config): + """Update the logger with config settings.""" + global logger + logger = setup_logging_with_config(config)