config.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. from dataclasses import dataclass
  2. import os
  3. @dataclass
  4. class GatewayConfig:
  5. auth_base_url: str
  6. tools_base_url: str
  7. client_type: str = 'workbuddy'
  8. timeout_seconds: int = 10
  9. refresh_skew_seconds: int = 120
  10. log_level: str = 'info'
  11. token_store_path: str = '.mcp_token.json'
  12. token_store_type: str = 'file'
  13. redis_host: str = '127.0.0.1'
  14. redis_port: int = 6379
  15. redis_db: int = 0
  16. redis_password: str = ''
  17. redis_prefix: str = 'fms:mcp:workbuddy:'
  18. session_key: str = ''
  19. @classmethod
  20. def from_env(cls, env=None, dotenv_path=''):
  21. primary_env = os.environ if env is None else env
  22. dotenv_path = str(dotenv_path or '').strip() or os.path.join(os.path.dirname(__file__), '.env')
  23. dotenv_env = cls._load_dotenv(dotenv_path)
  24. dotenv_shared_base = (dotenv_env.get('FMS_API_BASE') or '').rstrip('/')
  25. env_shared_base = (primary_env.get('FMS_API_BASE') or '').rstrip('/')
  26. auth_base_url = cls._resolve_base_url(
  27. dotenv_env,
  28. primary_env,
  29. dotenv_shared_base,
  30. env_shared_base,
  31. 'MCP_AUTH_BASE_URL',
  32. 'FMS_AUTH_BASE',
  33. )
  34. tools_base_url = cls._resolve_base_url(
  35. dotenv_env,
  36. primary_env,
  37. dotenv_shared_base,
  38. env_shared_base,
  39. 'MCP_TOOLS_BASE_URL',
  40. 'FMS_TOOLS_BASE',
  41. )
  42. timeout_seconds = cls._resolve_timeout_seconds(dotenv_env, primary_env)
  43. session_key = cls._pick(dotenv_env, primary_env, 'MCP_SESSION_KEY', 'FMS_SESSION_KEY')
  44. return cls(
  45. auth_base_url=auth_base_url,
  46. tools_base_url=tools_base_url,
  47. client_type=cls._pick(dotenv_env, primary_env, 'MCP_CLIENT_TYPE', 'FMS_CLIENT_TYPE') or 'workbuddy',
  48. timeout_seconds=timeout_seconds,
  49. refresh_skew_seconds=int(cls._pick(dotenv_env, primary_env, 'MCP_REFRESH_SKEW_SECONDS', 'FMS_REFRESH_SKEW_SECONDS') or '120'),
  50. log_level=(cls._pick(dotenv_env, primary_env, 'MCP_LOG_LEVEL', 'FMS_LOG_LEVEL') or 'info').lower(),
  51. token_store_path=cls._pick(dotenv_env, primary_env, 'MCP_TOKEN_STORE_PATH', 'FMS_TOKEN_STORE_PATH') or '.mcp_token.json',
  52. token_store_type=(cls._pick(dotenv_env, primary_env, 'MCP_TOKEN_STORE', 'FMS_TOKEN_STORE') or 'file').lower(),
  53. redis_host=cls._pick(dotenv_env, primary_env, 'MCP_REDIS_HOST', 'FMS_REDIS_HOST') or '127.0.0.1',
  54. redis_port=int(cls._pick(dotenv_env, primary_env, 'MCP_REDIS_PORT', 'FMS_REDIS_PORT') or '6379'),
  55. redis_db=int(cls._pick(dotenv_env, primary_env, 'MCP_REDIS_DB', 'FMS_REDIS_DB') or '0'),
  56. redis_password=cls._pick(dotenv_env, primary_env, 'MCP_REDIS_PASSWORD', 'FMS_REDIS_PASSWORD') or '',
  57. redis_prefix=cls._pick(dotenv_env, primary_env, 'MCP_REDIS_PREFIX', 'FMS_REDIS_PREFIX') or 'fms:mcp:workbuddy:',
  58. session_key=session_key or cls._build_default_session_key(primary_env),
  59. )
  60. @staticmethod
  61. def _resolve_base_url(preferred_env, fallback_env, preferred_shared_base, fallback_shared_base, *specific_keys):
  62. for key in specific_keys:
  63. value = preferred_env.get(key)
  64. if value:
  65. return value.rstrip('/')
  66. if preferred_shared_base:
  67. return preferred_shared_base
  68. for key in specific_keys:
  69. value = fallback_env.get(key)
  70. if value:
  71. return value.rstrip('/')
  72. return fallback_shared_base
  73. @staticmethod
  74. def _resolve_timeout_seconds(preferred_env, fallback_env):
  75. if preferred_env.get('MCP_TIMEOUT_SECONDS'):
  76. return int(preferred_env['MCP_TIMEOUT_SECONDS'])
  77. if preferred_env.get('FMS_TIMEOUT_SECONDS'):
  78. return int(preferred_env['FMS_TIMEOUT_SECONDS'])
  79. if preferred_env.get('FMS_TIMEOUT_MS'):
  80. return max(1, int(preferred_env['FMS_TIMEOUT_MS']) // 1000)
  81. if fallback_env.get('MCP_TIMEOUT_SECONDS'):
  82. return int(fallback_env['MCP_TIMEOUT_SECONDS'])
  83. if fallback_env.get('FMS_TIMEOUT_SECONDS'):
  84. return int(fallback_env['FMS_TIMEOUT_SECONDS'])
  85. if fallback_env.get('FMS_TIMEOUT_MS'):
  86. return max(1, int(fallback_env['FMS_TIMEOUT_MS']) // 1000)
  87. return 10
  88. @staticmethod
  89. def _pick(preferred_env, fallback_env, *keys):
  90. for key in keys:
  91. value = preferred_env.get(key)
  92. if value:
  93. return value
  94. for key in keys:
  95. value = fallback_env.get(key)
  96. if value:
  97. return value
  98. return ''
  99. @staticmethod
  100. def _build_default_session_key(env):
  101. computer = env.get('COMPUTERNAME') or env.get('HOSTNAME') or os.environ.get('COMPUTERNAME') or os.environ.get('HOSTNAME') or 'unknown-computer'
  102. user = env.get('USERNAME') or env.get('USER') or os.environ.get('USERNAME') or os.environ.get('USER') or 'unknown-user'
  103. domain = env.get('USERDOMAIN') or os.environ.get('USERDOMAIN') or ''
  104. parts = [domain, computer, user] if domain else [computer, user]
  105. return ':'.join(str(part).strip().replace(' ', '_') for part in parts if str(part).strip())
  106. @staticmethod
  107. def _load_dotenv(dotenv_path):
  108. data = {}
  109. if not dotenv_path or not os.path.exists(dotenv_path):
  110. return data
  111. with open(dotenv_path, 'r', encoding='utf-8') as file:
  112. for raw_line in file:
  113. line = raw_line.strip()
  114. if not line or line.startswith('#') or '=' not in line:
  115. continue
  116. key, value = line.split('=', 1)
  117. key = key.strip()
  118. value = value.strip().strip('"').strip("'")
  119. if key:
  120. data[key] = value
  121. return data