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)