diff --git a/archon-ui-main/src/components/settings/FeaturesSection.tsx b/archon-ui-main/src/components/settings/FeaturesSection.tsx index c827c8ac57..ed3d505cb7 100644 --- a/archon-ui-main/src/components/settings/FeaturesSection.tsx +++ b/archon-ui-main/src/components/settings/FeaturesSection.tsx @@ -39,7 +39,7 @@ export const FeaturesSection = () => { const [logfireResponse, projectsResponse, projectsHealthResponse, disconnectScreenRes] = await Promise.all([ credentialsService.getCredential('LOGFIRE_ENABLED').catch(() => ({ value: undefined })), credentialsService.getCredential('PROJECTS_ENABLED').catch(() => ({ value: undefined })), - fetch(`${credentialsService['baseUrl']}/api/projects/health`).catch(() => null), + fetch(`/api/projects/health`).catch(() => null), credentialsService.getCredential('DISCONNECT_SCREEN_ENABLED').catch(() => ({ value: 'true' })) ]); @@ -58,7 +58,7 @@ export const FeaturesSection = () => { response: projectsHealthResponse, ok: projectsHealthResponse?.ok, status: projectsHealthResponse?.status, - url: `${credentialsService['baseUrl']}/api/projects/health` + url: '/api/projects/health' }); if (projectsHealthResponse && projectsHealthResponse.ok) { diff --git a/archon-ui-main/src/features/mcp/components/McpStatusBar.tsx b/archon-ui-main/src/features/mcp/components/McpStatusBar.tsx index 3ed7a5b10c..4596449adf 100644 --- a/archon-ui-main/src/features/mcp/components/McpStatusBar.tsx +++ b/archon-ui-main/src/features/mcp/components/McpStatusBar.tsx @@ -77,7 +77,7 @@ export const McpStatusBar: React.FC = ({
MCP - 8051 + {config?.port ?? 8051}
{/* Active Sessions */} @@ -87,7 +87,7 @@ export const McpStatusBar: React.FC = ({
SESSIONS - Coming Soon + {sessionInfo.active_sessions ?? 0}
)} @@ -99,7 +99,7 @@ export const McpStatusBar: React.FC = ({ {config?.transport === 'streamable-http' ? 'HTTP' : config?.transport === 'sse' ? 'SSE' : - config?.transport || 'HTTP'} + (config?.transport || 'HTTP')} diff --git a/archon-ui-main/src/services/credentialsService.ts b/archon-ui-main/src/services/credentialsService.ts index bb14b48935..1d02989037 100644 --- a/archon-ui-main/src/services/credentialsService.ts +++ b/archon-ui-main/src/services/credentialsService.ts @@ -53,10 +53,7 @@ export interface CodeExtractionSettings { ENABLE_CODE_SUMMARIES: boolean; } -import { getApiUrl } from "../config/api"; - class CredentialsService { - private baseUrl = getApiUrl(); private handleCredentialError(error: any, context: string): Error { const errorMessage = error instanceof Error ? error.message : String(error); @@ -78,7 +75,7 @@ class CredentialsService { } async getAllCredentials(): Promise { - const response = await fetch(`${this.baseUrl}/api/credentials`); + const response = await fetch(`/api/credentials`); if (!response.ok) { throw new Error("Failed to fetch credentials"); } @@ -87,7 +84,7 @@ class CredentialsService { async getCredentialsByCategory(category: string): Promise { const response = await fetch( - `${this.baseUrl}/api/credentials/categories/${category}`, + `/api/credentials/categories/${category}`, ); if (!response.ok) { throw new Error(`Failed to fetch credentials for category: ${category}`); @@ -128,7 +125,7 @@ class CredentialsService { async getCredential( key: string, ): Promise<{ key: string; value?: string; is_encrypted?: boolean }> { - const response = await fetch(`${this.baseUrl}/api/credentials/${key}`); + const response = await fetch(`/api/credentials/${key}`); if (!response.ok) { if (response.status === 404) { // Return empty object if credential not found @@ -222,7 +219,7 @@ class CredentialsService { async updateCredential(credential: Credential): Promise { try { const response = await fetch( - `${this.baseUrl}/api/credentials/${credential.key}`, + `/api/credentials/${credential.key}`, { method: "PUT", headers: { @@ -248,7 +245,7 @@ class CredentialsService { async createCredential(credential: Credential): Promise { try { - const response = await fetch(`${this.baseUrl}/api/credentials`, { + const response = await fetch(`/api/credentials`, { method: "POST", headers: { "Content-Type": "application/json", @@ -272,7 +269,7 @@ class CredentialsService { async deleteCredential(key: string): Promise { try { - const response = await fetch(`${this.baseUrl}/api/credentials/${key}`, { + const response = await fetch(`/api/credentials/${key}`, { method: "DELETE", }); diff --git a/docker-compose.yml b/docker-compose.yml index a4c2c74764..8d1252d1fd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,7 +19,7 @@ services: - SERVICE_DISCOVERY_MODE=docker_compose - LOG_LEVEL=${LOG_LEVEL:-INFO} - ARCHON_SERVER_PORT=${ARCHON_SERVER_PORT:-8181} - - ARCHON_MCP_PORT=${ARCHON_MCP_PORT:-8051} + - ARCHON_MCP_PORT=${ARCHON_MCP_PORT:-9051} - ARCHON_AGENTS_PORT=${ARCHON_AGENTS_PORT:-8052} # Enable optional SSE/JSON-RPC handshake after REST probe fails - MCP_HANDSHAKE_ENABLED=${MCP_HANDSHAKE_ENABLED:-false} @@ -44,11 +44,11 @@ services: dockerfile: Dockerfile.mcp args: BUILDKIT_INLINE_CACHE: 1 - ARCHON_MCP_PORT: ${ARCHON_MCP_PORT:-8051} + ARCHON_MCP_PORT: ${ARCHON_MCP_PORT:-9051} container_name: Archon-MCP restart: unless-stopped ports: - - "${ARCHON_MCP_PORT:-8051}:${ARCHON_MCP_PORT:-8051}" + - "${ARCHON_MCP_PORT:-9051}:${ARCHON_MCP_PORT:-9051}" environment: - SUPABASE_URL=${SUPABASE_URL:-} - SUPABASE_SERVICE_KEY=${SUPABASE_SERVICE_KEY:-} @@ -68,7 +68,7 @@ services: - archon-server - archon-agents healthcheck: - test: ["CMD", "sh", "-c", "python -c \"import socket; s=socket.socket(); s.connect(('localhost', ${ARCHON_MCP_PORT:-8051})); s.close()\""] + test: ["CMD", "sh", "-c", "python -c \"import socket; s=socket.socket(); s.connect(('localhost', ${ARCHON_MCP_PORT:-9051})); s.close()\""] interval: 30s timeout: 10s retries: 3 diff --git a/python/src/server/api_routes/mcp_api.py b/python/src/server/api_routes/mcp_api.py index 615f100e89..81914223e9 100644 --- a/python/src/server/api_routes/mcp_api.py +++ b/python/src/server/api_routes/mcp_api.py @@ -767,11 +767,11 @@ async def get_mcp_config(request: Request): mcp_port = int(os.getenv("ARCHON_MCP_PORT", "8051")) - # Configuration for SSE-only mode with actual port + # Configuration for streamable-http (HTTP) mode with actual port config = { "host": "localhost", "port": mcp_port, - "transport": "sse", + "transport": "streamable-http", } # Get only model choice from database @@ -802,10 +802,10 @@ async def get_mcp_config(request: Request): config["use_agentic_rag"] = False config["use_reranking"] = False - api_logger.info("MCP configuration (SSE-only mode)") + api_logger.info("MCP configuration (streamable-http mode)") safe_set_attribute(span, "host", config["host"]) safe_set_attribute(span, "port", config["port"]) - safe_set_attribute(span, "transport", "sse") + safe_set_attribute(span, "transport", "streamable-http") safe_set_attribute(span, "model_choice", config.get("model_choice", "gpt-4o-mini")) return config diff --git a/python/src/server/services/credential_service.py b/python/src/server/services/credential_service.py index 017c3b2af1..659decfd7f 100644 --- a/python/src/server/services/credential_service.py +++ b/python/src/server/services/credential_service.py @@ -5,6 +5,7 @@ Credentials include API keys, service credentials, and application configuration. """ +import asyncio import base64 import os import re @@ -121,39 +122,65 @@ def _decrypt_value(self, encrypted_value: str) -> str: logger.error(f"Error decrypting value: {e}") raise - async def load_all_credentials(self) -> dict[str, Any]: - """Load all credentials from database and cache them.""" - try: - supabase = self._get_supabase_client() - - # Fetch all credentials - result = supabase.table("archon_settings").select("*").execute() + async def load_all_credentials( + self, max_retries: int = 10, initial_delay: float = 2.0 + ) -> dict[str, Any]: + """Load all credentials from database and cache them. + + Implements retry logic with exponential backoff to handle + Supabase unavailability (e.g., after pause/restore). + + Args: + max_retries: Maximum number of retry attempts (default: 10) + initial_delay: Initial delay in seconds between retries (default: 2.0) + """ + last_error = None + delay = initial_delay + + for attempt in range(1, max_retries + 1): + try: + supabase = self._get_supabase_client() + + # Fetch all credentials + result = supabase.table("archon_settings").select("*").execute() + + credentials = {} + for item in result.data: + key = item["key"] + if item["is_encrypted"] and item["encrypted_value"]: + # For encrypted values, we store the encrypted version + # Decryption happens when the value is actually needed + credentials[key] = { + "encrypted_value": item["encrypted_value"], + "is_encrypted": True, + "category": item["category"], + "description": item["description"], + } + else: + # Plain text values + credentials[key] = item["value"] + + self._cache = credentials + self._cache_initialized = True + logger.info(f"Loaded {len(credentials)} credentials from database") + + return credentials - credentials = {} - for item in result.data: - key = item["key"] - if item["is_encrypted"] and item["encrypted_value"]: - # For encrypted values, we store the encrypted version - # Decryption happens when the value is actually needed - credentials[key] = { - "encrypted_value": item["encrypted_value"], - "is_encrypted": True, - "category": item["category"], - "description": item["description"], - } + except Exception as e: + last_error = e + if attempt < max_retries: + logger.warning( + f"Supabase connection attempt {attempt}/{max_retries} failed: {e}. " + f"Retrying in {delay:.1f}s..." + ) + await asyncio.sleep(delay) + delay = min(delay * 2, 60.0) # Exponential backoff, max 60s else: - # Plain text values - credentials[key] = item["value"] - - self._cache = credentials - self._cache_initialized = True - logger.info(f"Loaded {len(credentials)} credentials from database") - - return credentials - - except Exception as e: - logger.error(f"Error loading credentials: {e}") - raise + logger.error( + f"Failed to connect to Supabase after {max_retries} attempts: {e}" + ) + + raise last_error async def get_credential(self, key: str, default: Any = None, decrypt: bool = True) -> Any: """Get a credential value by key."""