atlas-chat 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- atlas/__init__.py +40 -0
- atlas/application/__init__.py +7 -0
- atlas/application/chat/__init__.py +7 -0
- atlas/application/chat/agent/__init__.py +10 -0
- atlas/application/chat/agent/act_loop.py +179 -0
- atlas/application/chat/agent/factory.py +142 -0
- atlas/application/chat/agent/protocols.py +46 -0
- atlas/application/chat/agent/react_loop.py +338 -0
- atlas/application/chat/agent/think_act_loop.py +171 -0
- atlas/application/chat/approval_manager.py +151 -0
- atlas/application/chat/elicitation_manager.py +191 -0
- atlas/application/chat/events/__init__.py +1 -0
- atlas/application/chat/events/agent_event_relay.py +112 -0
- atlas/application/chat/modes/__init__.py +1 -0
- atlas/application/chat/modes/agent.py +125 -0
- atlas/application/chat/modes/plain.py +74 -0
- atlas/application/chat/modes/rag.py +81 -0
- atlas/application/chat/modes/tools.py +179 -0
- atlas/application/chat/orchestrator.py +213 -0
- atlas/application/chat/policies/__init__.py +1 -0
- atlas/application/chat/policies/tool_authorization.py +99 -0
- atlas/application/chat/preprocessors/__init__.py +1 -0
- atlas/application/chat/preprocessors/message_builder.py +92 -0
- atlas/application/chat/preprocessors/prompt_override_service.py +104 -0
- atlas/application/chat/service.py +454 -0
- atlas/application/chat/utilities/__init__.py +6 -0
- atlas/application/chat/utilities/error_handler.py +367 -0
- atlas/application/chat/utilities/event_notifier.py +546 -0
- atlas/application/chat/utilities/file_processor.py +613 -0
- atlas/application/chat/utilities/tool_executor.py +789 -0
- atlas/atlas_chat_cli.py +347 -0
- atlas/atlas_client.py +238 -0
- atlas/core/__init__.py +0 -0
- atlas/core/auth.py +205 -0
- atlas/core/authorization_manager.py +27 -0
- atlas/core/capabilities.py +123 -0
- atlas/core/compliance.py +215 -0
- atlas/core/domain_whitelist.py +147 -0
- atlas/core/domain_whitelist_middleware.py +82 -0
- atlas/core/http_client.py +28 -0
- atlas/core/log_sanitizer.py +102 -0
- atlas/core/metrics_logger.py +59 -0
- atlas/core/middleware.py +131 -0
- atlas/core/otel_config.py +242 -0
- atlas/core/prompt_risk.py +200 -0
- atlas/core/rate_limit.py +0 -0
- atlas/core/rate_limit_middleware.py +64 -0
- atlas/core/security_headers_middleware.py +51 -0
- atlas/domain/__init__.py +37 -0
- atlas/domain/chat/__init__.py +1 -0
- atlas/domain/chat/dtos.py +85 -0
- atlas/domain/errors.py +96 -0
- atlas/domain/messages/__init__.py +12 -0
- atlas/domain/messages/models.py +160 -0
- atlas/domain/rag_mcp_service.py +664 -0
- atlas/domain/sessions/__init__.py +7 -0
- atlas/domain/sessions/models.py +36 -0
- atlas/domain/unified_rag_service.py +371 -0
- atlas/infrastructure/__init__.py +10 -0
- atlas/infrastructure/app_factory.py +135 -0
- atlas/infrastructure/events/__init__.py +1 -0
- atlas/infrastructure/events/cli_event_publisher.py +140 -0
- atlas/infrastructure/events/websocket_publisher.py +140 -0
- atlas/infrastructure/sessions/in_memory_repository.py +56 -0
- atlas/infrastructure/transport/__init__.py +7 -0
- atlas/infrastructure/transport/websocket_connection_adapter.py +33 -0
- atlas/init_cli.py +226 -0
- atlas/interfaces/__init__.py +15 -0
- atlas/interfaces/events.py +134 -0
- atlas/interfaces/llm.py +54 -0
- atlas/interfaces/rag.py +40 -0
- atlas/interfaces/sessions.py +75 -0
- atlas/interfaces/tools.py +57 -0
- atlas/interfaces/transport.py +24 -0
- atlas/main.py +564 -0
- atlas/mcp/api_key_demo/README.md +76 -0
- atlas/mcp/api_key_demo/main.py +172 -0
- atlas/mcp/api_key_demo/run.sh +56 -0
- atlas/mcp/basictable/main.py +147 -0
- atlas/mcp/calculator/main.py +149 -0
- atlas/mcp/code-executor/execution_engine.py +98 -0
- atlas/mcp/code-executor/execution_environment.py +95 -0
- atlas/mcp/code-executor/main.py +528 -0
- atlas/mcp/code-executor/result_processing.py +276 -0
- atlas/mcp/code-executor/script_generation.py +195 -0
- atlas/mcp/code-executor/security_checker.py +140 -0
- atlas/mcp/corporate_cars/main.py +437 -0
- atlas/mcp/csv_reporter/main.py +545 -0
- atlas/mcp/duckduckgo/main.py +182 -0
- atlas/mcp/elicitation_demo/README.md +171 -0
- atlas/mcp/elicitation_demo/main.py +262 -0
- atlas/mcp/env-demo/README.md +158 -0
- atlas/mcp/env-demo/main.py +199 -0
- atlas/mcp/file_size_test/main.py +284 -0
- atlas/mcp/filesystem/main.py +348 -0
- atlas/mcp/image_demo/main.py +113 -0
- atlas/mcp/image_demo/requirements.txt +4 -0
- atlas/mcp/logging_demo/README.md +72 -0
- atlas/mcp/logging_demo/main.py +103 -0
- atlas/mcp/many_tools_demo/main.py +50 -0
- atlas/mcp/order_database/__init__.py +0 -0
- atlas/mcp/order_database/main.py +369 -0
- atlas/mcp/order_database/signal_data.csv +1001 -0
- atlas/mcp/pdfbasic/main.py +394 -0
- atlas/mcp/pptx_generator/main.py +760 -0
- atlas/mcp/pptx_generator/requirements.txt +13 -0
- atlas/mcp/pptx_generator/run_test.sh +1 -0
- atlas/mcp/pptx_generator/test_pptx_generator_security.py +169 -0
- atlas/mcp/progress_demo/main.py +167 -0
- atlas/mcp/progress_updates_demo/QUICKSTART.md +273 -0
- atlas/mcp/progress_updates_demo/README.md +120 -0
- atlas/mcp/progress_updates_demo/main.py +497 -0
- atlas/mcp/prompts/main.py +222 -0
- atlas/mcp/public_demo/main.py +189 -0
- atlas/mcp/sampling_demo/README.md +169 -0
- atlas/mcp/sampling_demo/main.py +234 -0
- atlas/mcp/thinking/main.py +77 -0
- atlas/mcp/tool_planner/main.py +240 -0
- atlas/mcp/ui-demo/badmesh.png +0 -0
- atlas/mcp/ui-demo/main.py +383 -0
- atlas/mcp/ui-demo/templates/button_demo.html +32 -0
- atlas/mcp/ui-demo/templates/data_visualization.html +32 -0
- atlas/mcp/ui-demo/templates/form_demo.html +28 -0
- atlas/mcp/username-override-demo/README.md +320 -0
- atlas/mcp/username-override-demo/main.py +308 -0
- atlas/modules/__init__.py +0 -0
- atlas/modules/config/__init__.py +34 -0
- atlas/modules/config/cli.py +231 -0
- atlas/modules/config/config_manager.py +1096 -0
- atlas/modules/file_storage/__init__.py +22 -0
- atlas/modules/file_storage/cli.py +330 -0
- atlas/modules/file_storage/content_extractor.py +290 -0
- atlas/modules/file_storage/manager.py +295 -0
- atlas/modules/file_storage/mock_s3_client.py +402 -0
- atlas/modules/file_storage/s3_client.py +417 -0
- atlas/modules/llm/__init__.py +19 -0
- atlas/modules/llm/caller.py +287 -0
- atlas/modules/llm/litellm_caller.py +675 -0
- atlas/modules/llm/models.py +19 -0
- atlas/modules/mcp_tools/__init__.py +17 -0
- atlas/modules/mcp_tools/client.py +2123 -0
- atlas/modules/mcp_tools/token_storage.py +556 -0
- atlas/modules/prompts/prompt_provider.py +130 -0
- atlas/modules/rag/__init__.py +24 -0
- atlas/modules/rag/atlas_rag_client.py +336 -0
- atlas/modules/rag/client.py +129 -0
- atlas/routes/admin_routes.py +865 -0
- atlas/routes/config_routes.py +484 -0
- atlas/routes/feedback_routes.py +361 -0
- atlas/routes/files_routes.py +274 -0
- atlas/routes/health_routes.py +40 -0
- atlas/routes/mcp_auth_routes.py +223 -0
- atlas/server_cli.py +164 -0
- atlas/tests/conftest.py +20 -0
- atlas/tests/integration/test_mcp_auth_integration.py +152 -0
- atlas/tests/manual_test_sampling.py +87 -0
- atlas/tests/modules/mcp_tools/test_client_auth.py +226 -0
- atlas/tests/modules/mcp_tools/test_client_env.py +191 -0
- atlas/tests/test_admin_mcp_server_management_routes.py +141 -0
- atlas/tests/test_agent_roa.py +135 -0
- atlas/tests/test_app_factory_smoke.py +47 -0
- atlas/tests/test_approval_manager.py +439 -0
- atlas/tests/test_atlas_client.py +188 -0
- atlas/tests/test_atlas_rag_client.py +447 -0
- atlas/tests/test_atlas_rag_integration.py +224 -0
- atlas/tests/test_attach_file_flow.py +287 -0
- atlas/tests/test_auth_utils.py +165 -0
- atlas/tests/test_backend_public_url.py +185 -0
- atlas/tests/test_banner_logging.py +287 -0
- atlas/tests/test_capability_tokens_and_injection.py +203 -0
- atlas/tests/test_compliance_level.py +54 -0
- atlas/tests/test_compliance_manager.py +253 -0
- atlas/tests/test_config_manager.py +617 -0
- atlas/tests/test_config_manager_paths.py +12 -0
- atlas/tests/test_core_auth.py +18 -0
- atlas/tests/test_core_utils.py +190 -0
- atlas/tests/test_docker_env_sync.py +202 -0
- atlas/tests/test_domain_errors.py +329 -0
- atlas/tests/test_domain_whitelist.py +359 -0
- atlas/tests/test_elicitation_manager.py +408 -0
- atlas/tests/test_elicitation_routing.py +296 -0
- atlas/tests/test_env_demo_server.py +88 -0
- atlas/tests/test_error_classification.py +113 -0
- atlas/tests/test_error_flow_integration.py +116 -0
- atlas/tests/test_feedback_routes.py +333 -0
- atlas/tests/test_file_content_extraction.py +1134 -0
- atlas/tests/test_file_extraction_routes.py +158 -0
- atlas/tests/test_file_library.py +107 -0
- atlas/tests/test_file_manager_unit.py +18 -0
- atlas/tests/test_health_route.py +49 -0
- atlas/tests/test_http_client_stub.py +8 -0
- atlas/tests/test_imports_smoke.py +30 -0
- atlas/tests/test_interfaces_llm_response.py +9 -0
- atlas/tests/test_issue_access_denied_fix.py +136 -0
- atlas/tests/test_llm_env_expansion.py +836 -0
- atlas/tests/test_log_level_sensitive_data.py +285 -0
- atlas/tests/test_mcp_auth_routes.py +341 -0
- atlas/tests/test_mcp_client_auth.py +331 -0
- atlas/tests/test_mcp_data_injection.py +270 -0
- atlas/tests/test_mcp_get_authorized_servers.py +95 -0
- atlas/tests/test_mcp_hot_reload.py +512 -0
- atlas/tests/test_mcp_image_content.py +424 -0
- atlas/tests/test_mcp_logging.py +172 -0
- atlas/tests/test_mcp_progress_updates.py +313 -0
- atlas/tests/test_mcp_prompt_override_system_prompt.py +102 -0
- atlas/tests/test_mcp_prompts_server.py +39 -0
- atlas/tests/test_mcp_tool_result_parsing.py +296 -0
- atlas/tests/test_metrics_logger.py +56 -0
- atlas/tests/test_middleware_auth.py +379 -0
- atlas/tests/test_prompt_risk_and_acl.py +141 -0
- atlas/tests/test_rag_mcp_aggregator.py +204 -0
- atlas/tests/test_rag_mcp_service.py +224 -0
- atlas/tests/test_rate_limit_middleware.py +45 -0
- atlas/tests/test_routes_config_smoke.py +60 -0
- atlas/tests/test_routes_files_download_token.py +41 -0
- atlas/tests/test_routes_files_health.py +18 -0
- atlas/tests/test_runtime_imports.py +53 -0
- atlas/tests/test_sampling_integration.py +482 -0
- atlas/tests/test_security_admin_routes.py +61 -0
- atlas/tests/test_security_capability_tokens.py +65 -0
- atlas/tests/test_security_file_stats_scope.py +21 -0
- atlas/tests/test_security_header_injection.py +191 -0
- atlas/tests/test_security_headers_and_filename.py +63 -0
- atlas/tests/test_shared_session_repository.py +101 -0
- atlas/tests/test_system_prompt_loading.py +181 -0
- atlas/tests/test_token_storage.py +505 -0
- atlas/tests/test_tool_approval_config.py +93 -0
- atlas/tests/test_tool_approval_utils.py +356 -0
- atlas/tests/test_tool_authorization_group_filtering.py +223 -0
- atlas/tests/test_tool_details_in_config.py +108 -0
- atlas/tests/test_tool_planner.py +300 -0
- atlas/tests/test_unified_rag_service.py +398 -0
- atlas/tests/test_username_override_in_approval.py +258 -0
- atlas/tests/test_websocket_auth_header.py +168 -0
- atlas/version.py +6 -0
- atlas_chat-0.1.0.data/data/.env.example +253 -0
- atlas_chat-0.1.0.data/data/config/defaults/compliance-levels.json +44 -0
- atlas_chat-0.1.0.data/data/config/defaults/domain-whitelist.json +123 -0
- atlas_chat-0.1.0.data/data/config/defaults/file-extractors.json +74 -0
- atlas_chat-0.1.0.data/data/config/defaults/help-config.json +198 -0
- atlas_chat-0.1.0.data/data/config/defaults/llmconfig-buggy.yml +11 -0
- atlas_chat-0.1.0.data/data/config/defaults/llmconfig.yml +19 -0
- atlas_chat-0.1.0.data/data/config/defaults/mcp.json +138 -0
- atlas_chat-0.1.0.data/data/config/defaults/rag-sources.json +17 -0
- atlas_chat-0.1.0.data/data/config/defaults/splash-config.json +16 -0
- atlas_chat-0.1.0.dist-info/METADATA +236 -0
- atlas_chat-0.1.0.dist-info/RECORD +250 -0
- atlas_chat-0.1.0.dist-info/WHEEL +5 -0
- atlas_chat-0.1.0.dist-info/entry_points.txt +4 -0
- atlas_chat-0.1.0.dist-info/top_level.txt +1 -0
atlas/main.py
ADDED
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Basic chat backend implementing the modular architecture.
|
|
3
|
+
Focuses on essential chat functionality only.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
# Suppress LiteLLM verbose logging BEFORE any transitive import of litellm.
|
|
7
|
+
# litellm._logging reads LITELLM_LOG at import time and defaults to DEBUG.
|
|
8
|
+
# This must happen before any other imports that might load litellm.
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path as _Path
|
|
11
|
+
|
|
12
|
+
from dotenv import dotenv_values as _dotenv_values
|
|
13
|
+
|
|
14
|
+
# Load .env values without setting them in os.environ yet (just to read feature flag)
|
|
15
|
+
_env_path = _Path(__file__).parent.parent / ".env"
|
|
16
|
+
_env_values = _dotenv_values(_env_path) if _env_path.exists() else {}
|
|
17
|
+
|
|
18
|
+
# Check feature flag: FEATURE_SUPPRESS_LITELLM_LOGGING (default: true)
|
|
19
|
+
_suppress_litellm = _env_values.get("FEATURE_SUPPRESS_LITELLM_LOGGING", "true").lower() in ("true", "1", "yes")
|
|
20
|
+
|
|
21
|
+
if _suppress_litellm and "LITELLM_LOG" not in os.environ:
|
|
22
|
+
os.environ["LITELLM_LOG"] = "ERROR"
|
|
23
|
+
|
|
24
|
+
# Clean up temporary imports
|
|
25
|
+
del _Path, _dotenv_values, _env_path, _env_values, _suppress_litellm
|
|
26
|
+
|
|
27
|
+
# Standard imports follow - must come after LiteLLM logging suppression above
|
|
28
|
+
# ruff: noqa: E402
|
|
29
|
+
import asyncio
|
|
30
|
+
import logging
|
|
31
|
+
from contextlib import asynccontextmanager
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from uuid import uuid4
|
|
34
|
+
|
|
35
|
+
from dotenv import load_dotenv
|
|
36
|
+
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, WebSocketException
|
|
37
|
+
from fastapi.responses import FileResponse
|
|
38
|
+
from fastapi.staticfiles import StaticFiles
|
|
39
|
+
|
|
40
|
+
from atlas.core.auth import get_user_from_header
|
|
41
|
+
from atlas.core.domain_whitelist_middleware import DomainWhitelistMiddleware
|
|
42
|
+
from atlas.core.log_sanitizer import sanitize_for_logging, summarize_tool_approval_response_for_logging
|
|
43
|
+
from atlas.core.metrics_logger import log_metric
|
|
44
|
+
|
|
45
|
+
# Import from atlas.core (only essential middleware and config)
|
|
46
|
+
from atlas.core.middleware import AuthMiddleware
|
|
47
|
+
from atlas.core.otel_config import setup_opentelemetry
|
|
48
|
+
from atlas.core.rate_limit_middleware import RateLimitMiddleware
|
|
49
|
+
from atlas.core.security_headers_middleware import SecurityHeadersMiddleware
|
|
50
|
+
|
|
51
|
+
# Import domain errors
|
|
52
|
+
from atlas.domain.errors import DomainError, LLMAuthenticationError, LLMTimeoutError, RateLimitError, ValidationError
|
|
53
|
+
|
|
54
|
+
# Import from atlas.infrastructure
|
|
55
|
+
from atlas.infrastructure.app_factory import app_factory
|
|
56
|
+
from atlas.infrastructure.transport.websocket_connection_adapter import WebSocketConnectionAdapter
|
|
57
|
+
from atlas.routes.admin_routes import admin_router
|
|
58
|
+
|
|
59
|
+
# Import essential routes
|
|
60
|
+
from atlas.routes.config_routes import router as config_router
|
|
61
|
+
from atlas.routes.feedback_routes import feedback_router
|
|
62
|
+
from atlas.routes.files_routes import router as files_router
|
|
63
|
+
from atlas.routes.health_routes import router as health_router
|
|
64
|
+
from atlas.routes.mcp_auth_routes import router as mcp_auth_router
|
|
65
|
+
from atlas.version import VERSION
|
|
66
|
+
|
|
67
|
+
# Load environment variables from the parent directory
|
|
68
|
+
load_dotenv(dotenv_path="../.env")
|
|
69
|
+
|
|
70
|
+
# Setup OpenTelemetry logging
|
|
71
|
+
otel_config = setup_opentelemetry("atlas-ui-3-backend", "1.0.0")
|
|
72
|
+
|
|
73
|
+
logger = logging.getLogger(__name__)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
async def websocket_update_callback(websocket: WebSocket, message: dict):
|
|
77
|
+
"""
|
|
78
|
+
Callback function to handle websocket updates with logging.
|
|
79
|
+
"""
|
|
80
|
+
try:
|
|
81
|
+
mtype = message.get("type")
|
|
82
|
+
if mtype == "intermediate_update":
|
|
83
|
+
utype = message.get("update_type") or message.get("data", {}).get("update_type")
|
|
84
|
+
# Handle specific update types (canvas_files, files_update)
|
|
85
|
+
# Logging disabled for these message types - see git history if needed
|
|
86
|
+
if utype in ("canvas_files", "files_update"):
|
|
87
|
+
pass
|
|
88
|
+
elif mtype == "canvas_content":
|
|
89
|
+
content = message.get("content")
|
|
90
|
+
clen = len(content) if isinstance(content, str) else "obj"
|
|
91
|
+
logger.debug("WS SEND: canvas_content length=%s", clen)
|
|
92
|
+
else:
|
|
93
|
+
logger.debug("WS SEND: %s", mtype)
|
|
94
|
+
except Exception:
|
|
95
|
+
# Non-fatal logging error; continue to send
|
|
96
|
+
pass
|
|
97
|
+
await websocket.send_json(message)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _ensure_feedback_directory():
|
|
101
|
+
"""Ensure feedback storage directory exists at startup."""
|
|
102
|
+
from pathlib import Path
|
|
103
|
+
config = app_factory.get_config_manager()
|
|
104
|
+
feedback_dir = Path(config.app_settings.runtime_feedback_dir)
|
|
105
|
+
try:
|
|
106
|
+
feedback_dir.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
logger.info(f"Feedback directory ready: {feedback_dir}")
|
|
108
|
+
except Exception as e:
|
|
109
|
+
logger.warning(f"Could not create feedback directory {feedback_dir}: {e}")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@asynccontextmanager
|
|
113
|
+
async def lifespan(app: FastAPI):
|
|
114
|
+
"""Application lifespan manager."""
|
|
115
|
+
logger.info("Starting Chat UI Backend with modular architecture")
|
|
116
|
+
|
|
117
|
+
# Initialize configuration
|
|
118
|
+
config = app_factory.get_config_manager()
|
|
119
|
+
|
|
120
|
+
# SECURITY WARNING: Check for missing proxy secret in production
|
|
121
|
+
if not config.app_settings.debug_mode:
|
|
122
|
+
if not config.app_settings.feature_proxy_secret_enabled:
|
|
123
|
+
logger.warning(
|
|
124
|
+
"SECURITY WARNING: Proxy secret validation is DISABLED in production. "
|
|
125
|
+
"Set FEATURE_PROXY_SECRET_ENABLED=true and PROXY_SECRET to enable."
|
|
126
|
+
)
|
|
127
|
+
elif not config.app_settings.proxy_secret:
|
|
128
|
+
logger.warning(
|
|
129
|
+
"SECURITY WARNING: Proxy secret is ENABLED but PROXY_SECRET is not set. "
|
|
130
|
+
"Authentication will fail for all requests."
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
logger.info(f"Backend initialized with {len(config.llm_config.models)} LLM models")
|
|
134
|
+
logger.info(f"MCP servers configured: {len(config.mcp_config.servers)}")
|
|
135
|
+
|
|
136
|
+
# Ensure feedback directory exists
|
|
137
|
+
_ensure_feedback_directory()
|
|
138
|
+
|
|
139
|
+
# Initialize MCP tools manager
|
|
140
|
+
logger.info("Initializing MCP tools manager...")
|
|
141
|
+
mcp_manager = app_factory.get_mcp_manager()
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
logger.info("Step 1: Initializing MCP clients...")
|
|
145
|
+
await mcp_manager.initialize_clients()
|
|
146
|
+
logger.info("Step 1 complete: MCP clients initialized")
|
|
147
|
+
|
|
148
|
+
logger.info("Step 2: Discovering tools...")
|
|
149
|
+
await mcp_manager.discover_tools()
|
|
150
|
+
logger.info("Step 2 complete: Tool discovery finished")
|
|
151
|
+
|
|
152
|
+
logger.info("Step 3: Discovering prompts...")
|
|
153
|
+
await mcp_manager.discover_prompts()
|
|
154
|
+
logger.info("Step 3 complete: Prompt discovery finished")
|
|
155
|
+
|
|
156
|
+
logger.info("MCP tools manager initialization complete")
|
|
157
|
+
|
|
158
|
+
# Start auto-reconnect background task if enabled
|
|
159
|
+
logger.info("Step 4: Starting MCP auto-reconnect (if enabled)...")
|
|
160
|
+
await mcp_manager.start_auto_reconnect()
|
|
161
|
+
logger.info("Step 4 complete: Auto-reconnect task started (if enabled)")
|
|
162
|
+
|
|
163
|
+
except Exception as e:
|
|
164
|
+
logger.error(f"Error during MCP initialization: {e}", exc_info=True)
|
|
165
|
+
# Continue startup even if MCP fails
|
|
166
|
+
logger.warning("Continuing startup without MCP tools")
|
|
167
|
+
|
|
168
|
+
yield
|
|
169
|
+
|
|
170
|
+
logger.info("Shutting down Chat UI Backend")
|
|
171
|
+
# Stop auto-reconnect task
|
|
172
|
+
await mcp_manager.stop_auto_reconnect()
|
|
173
|
+
# Cleanup MCP clients
|
|
174
|
+
await mcp_manager.cleanup()
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# Create FastAPI app with minimal setup
|
|
178
|
+
app = FastAPI(
|
|
179
|
+
title="Chat UI Backend",
|
|
180
|
+
description="Basic chat backend with modular architecture",
|
|
181
|
+
version=VERSION,
|
|
182
|
+
lifespan=lifespan,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Get config for middleware
|
|
186
|
+
config = app_factory.get_config_manager()
|
|
187
|
+
|
|
188
|
+
"""Security: enforce rate limiting and auth middleware.
|
|
189
|
+
RateLimit first to cheaply throttle abusive traffic before heavier logic.
|
|
190
|
+
"""
|
|
191
|
+
app.add_middleware(SecurityHeadersMiddleware)
|
|
192
|
+
app.add_middleware(RateLimitMiddleware)
|
|
193
|
+
# Domain whitelist check (if enabled) - add before Auth so it runs after
|
|
194
|
+
if config.app_settings.feature_domain_whitelist_enabled:
|
|
195
|
+
app.add_middleware(
|
|
196
|
+
DomainWhitelistMiddleware,
|
|
197
|
+
auth_redirect_url=config.app_settings.auth_redirect_url
|
|
198
|
+
)
|
|
199
|
+
app.add_middleware(
|
|
200
|
+
AuthMiddleware,
|
|
201
|
+
debug_mode=config.app_settings.debug_mode,
|
|
202
|
+
auth_header_name=config.app_settings.auth_user_header,
|
|
203
|
+
auth_header_type=config.app_settings.auth_user_header_type,
|
|
204
|
+
auth_aws_expected_alb_arn=config.app_settings.auth_aws_expected_alb_arn,
|
|
205
|
+
auth_aws_region=config.app_settings.auth_aws_region,
|
|
206
|
+
proxy_secret_enabled=config.app_settings.feature_proxy_secret_enabled,
|
|
207
|
+
proxy_secret_header=config.app_settings.proxy_secret_header,
|
|
208
|
+
proxy_secret=config.app_settings.proxy_secret,
|
|
209
|
+
auth_redirect_url=config.app_settings.auth_redirect_url
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# Include essential routes (add files API)
|
|
213
|
+
app.include_router(config_router)
|
|
214
|
+
app.include_router(admin_router)
|
|
215
|
+
app.include_router(files_router)
|
|
216
|
+
app.include_router(health_router)
|
|
217
|
+
app.include_router(feedback_router)
|
|
218
|
+
app.include_router(mcp_auth_router)
|
|
219
|
+
|
|
220
|
+
# Serve frontend build (Vite)
|
|
221
|
+
project_root = Path(__file__).resolve().parents[1]
|
|
222
|
+
static_dir = project_root / "frontend" / "dist"
|
|
223
|
+
if static_dir.exists():
|
|
224
|
+
# Serve the SPA entry
|
|
225
|
+
@app.get("/")
|
|
226
|
+
async def read_root():
|
|
227
|
+
return FileResponse(str(static_dir / "index.html"))
|
|
228
|
+
|
|
229
|
+
# Serve hashed asset files under /assets (CSS/JS/images from Vite build)
|
|
230
|
+
assets_dir = static_dir / "assets"
|
|
231
|
+
if assets_dir.exists():
|
|
232
|
+
app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")
|
|
233
|
+
|
|
234
|
+
# Serve webfonts from Vite build (placed via frontend/public/fonts)
|
|
235
|
+
fonts_dir = static_dir / "fonts"
|
|
236
|
+
if fonts_dir.exists():
|
|
237
|
+
app.mount("/fonts", StaticFiles(directory=fonts_dir), name="fonts")
|
|
238
|
+
else:
|
|
239
|
+
# Fallback to unbuilt public fonts if dist/fonts is missing
|
|
240
|
+
public_fonts = project_root / "frontend" / "public" / "fonts"
|
|
241
|
+
if public_fonts.exists():
|
|
242
|
+
app.mount("/fonts", StaticFiles(directory=public_fonts), name="fonts")
|
|
243
|
+
|
|
244
|
+
# Common top-level static files in the Vite build
|
|
245
|
+
@app.get("/favicon.ico")
|
|
246
|
+
async def favicon():
|
|
247
|
+
path = static_dir / "favicon.ico"
|
|
248
|
+
return FileResponse(str(path))
|
|
249
|
+
|
|
250
|
+
@app.get("/vite.svg")
|
|
251
|
+
async def vite_svg():
|
|
252
|
+
path = static_dir / "vite.svg"
|
|
253
|
+
return FileResponse(str(path))
|
|
254
|
+
|
|
255
|
+
@app.get("/logo.png")
|
|
256
|
+
async def logo_png():
|
|
257
|
+
path = static_dir / "logo.png"
|
|
258
|
+
return FileResponse(str(path))
|
|
259
|
+
|
|
260
|
+
@app.get("/sandia-powered-by-atlas.png")
|
|
261
|
+
async def logo2_png():
|
|
262
|
+
path = static_dir / "sandia-powered-by-atlas.png"
|
|
263
|
+
return FileResponse(str(path))
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
# WebSocket endpoint for chat
|
|
267
|
+
@app.websocket("/ws")
|
|
268
|
+
async def websocket_endpoint(websocket: WebSocket):
|
|
269
|
+
"""
|
|
270
|
+
Main chat WebSocket endpoint using new architecture.
|
|
271
|
+
|
|
272
|
+
SECURITY NOTE - Production Architecture:
|
|
273
|
+
==========================================
|
|
274
|
+
This endpoint appears to lack authentication when viewed in isolation,
|
|
275
|
+
but in production it sits behind a reverse proxy with a separate
|
|
276
|
+
authentication service. The authentication flow is:
|
|
277
|
+
|
|
278
|
+
1. Client connects to WebSocket endpoint
|
|
279
|
+
2. Reverse proxy intercepts WebSocket handshake (HTTP Upgrade request)
|
|
280
|
+
3. Reverse proxy delegates to authentication service
|
|
281
|
+
4. Auth service validates JWT/session from cookies or headers
|
|
282
|
+
5. If valid: Auth service returns authenticated user header
|
|
283
|
+
6. Reverse proxy forwards connection to this app with authenticated user header
|
|
284
|
+
7. This app trusts the header (already validated by auth service)
|
|
285
|
+
|
|
286
|
+
The header name is configurable via AUTH_USER_HEADER environment variable
|
|
287
|
+
(default: X-User-Email). This allows flexibility for different reverse proxy setups.
|
|
288
|
+
|
|
289
|
+
SECURITY REQUIREMENTS:
|
|
290
|
+
- This app MUST ONLY be accessible via reverse proxy
|
|
291
|
+
- Direct public access to this app bypasses authentication
|
|
292
|
+
- Use network isolation to prevent direct access
|
|
293
|
+
- The /login endpoint lives in the separate auth service
|
|
294
|
+
- Reverse proxy MUST strip client-provided X-User-Email headers before adding its own
|
|
295
|
+
(otherwise attackers can inject headers: X-User-Email: admin@company.com)
|
|
296
|
+
|
|
297
|
+
DEVELOPMENT vs PRODUCTION:
|
|
298
|
+
- Production: Extracts user from configured auth header (set by reverse proxy)
|
|
299
|
+
- Development: Falls back to 'user' query parameter (INSECURE, local only)
|
|
300
|
+
|
|
301
|
+
See docs/security_architecture.md for complete architecture details.
|
|
302
|
+
"""
|
|
303
|
+
# Extract user email using the same authentication flow as HTTP requests
|
|
304
|
+
# Priority: 1) configured auth header (production), 2) query param (dev), 3) test user (dev fallback)
|
|
305
|
+
config_manager = app_factory.get_config_manager()
|
|
306
|
+
|
|
307
|
+
is_debug_mode = config_manager.app_settings.debug_mode
|
|
308
|
+
|
|
309
|
+
# WebSocket connections must present the shared proxy secret (same as AuthMiddleware)
|
|
310
|
+
if (
|
|
311
|
+
config_manager.app_settings.feature_proxy_secret_enabled
|
|
312
|
+
and config_manager.app_settings.proxy_secret
|
|
313
|
+
and not is_debug_mode
|
|
314
|
+
):
|
|
315
|
+
proxy_secret_header = config_manager.app_settings.proxy_secret_header
|
|
316
|
+
proxy_secret_value = websocket.headers.get(proxy_secret_header)
|
|
317
|
+
if proxy_secret_value != config_manager.app_settings.proxy_secret:
|
|
318
|
+
logger.warning(
|
|
319
|
+
"WS proxy secret mismatch on %s",
|
|
320
|
+
sanitize_for_logging(websocket.client)
|
|
321
|
+
)
|
|
322
|
+
raise WebSocketException(code=1008, reason="Invalid proxy secret")
|
|
323
|
+
|
|
324
|
+
# Authenticate user BEFORE accepting the connection
|
|
325
|
+
user_email = None
|
|
326
|
+
|
|
327
|
+
# Check configured auth header first (consistent with AuthMiddleware)
|
|
328
|
+
auth_header_name = config_manager.app_settings.auth_user_header
|
|
329
|
+
x_email_header = websocket.headers.get(auth_header_name)
|
|
330
|
+
if x_email_header:
|
|
331
|
+
user_email = get_user_from_header(x_email_header)
|
|
332
|
+
|
|
333
|
+
# Fallback to query parameter (development/testing ONLY)
|
|
334
|
+
if not user_email and is_debug_mode:
|
|
335
|
+
user_email = websocket.query_params.get('user')
|
|
336
|
+
if user_email:
|
|
337
|
+
logger.info(
|
|
338
|
+
"WebSocket authenticated via query parameter (debug mode): %s",
|
|
339
|
+
sanitize_for_logging(user_email)
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
# Final fallback to test user (development mode ONLY)
|
|
343
|
+
if not user_email and is_debug_mode:
|
|
344
|
+
user_email = config_manager.app_settings.test_user or 'test@test.com'
|
|
345
|
+
logger.info(
|
|
346
|
+
"WebSocket using fallback test user (debug mode): %s",
|
|
347
|
+
sanitize_for_logging(user_email)
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
# PRODUCTION: Reject unauthenticated connections
|
|
351
|
+
if not user_email:
|
|
352
|
+
logger.warning(
|
|
353
|
+
"WebSocket authentication failed - no user found in %s header. Client: %s",
|
|
354
|
+
sanitize_for_logging(auth_header_name),
|
|
355
|
+
sanitize_for_logging(websocket.client)
|
|
356
|
+
)
|
|
357
|
+
raise WebSocketException(
|
|
358
|
+
code=1008,
|
|
359
|
+
reason="Authentication required. Please ensure you are accessing this application through the configured reverse proxy."
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
# Now accept the connection (user is authenticated)
|
|
363
|
+
await websocket.accept()
|
|
364
|
+
logger.info(
|
|
365
|
+
"WebSocket authenticated via %s header: %s",
|
|
366
|
+
sanitize_for_logging(auth_header_name),
|
|
367
|
+
sanitize_for_logging(user_email)
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
session_id = uuid4()
|
|
371
|
+
|
|
372
|
+
# Create connection adapter with authenticated user and chat service
|
|
373
|
+
connection_adapter = WebSocketConnectionAdapter(websocket, user_email)
|
|
374
|
+
chat_service = app_factory.create_chat_service(connection_adapter)
|
|
375
|
+
|
|
376
|
+
logger.info(f"WebSocket connection established for session {sanitize_for_logging(str(session_id))}")
|
|
377
|
+
|
|
378
|
+
try:
|
|
379
|
+
while True:
|
|
380
|
+
data = await websocket.receive_json()
|
|
381
|
+
message_type = data.get("type")
|
|
382
|
+
|
|
383
|
+
# Debug: Log ALL incoming messages
|
|
384
|
+
logger.debug(
|
|
385
|
+
"WS RECEIVED message_type=[%s], data keys=%s",
|
|
386
|
+
sanitize_for_logging(message_type),
|
|
387
|
+
[f"[{sanitize_for_logging(key)}]" for key in data.keys()]
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
if message_type == "chat":
|
|
391
|
+
# Handle chat message in background so we can still receive approval responses
|
|
392
|
+
async def handle_chat():
|
|
393
|
+
try:
|
|
394
|
+
await chat_service.handle_chat_message(
|
|
395
|
+
session_id=session_id,
|
|
396
|
+
content=data.get("content", ""),
|
|
397
|
+
model=data.get("model", ""),
|
|
398
|
+
selected_tools=data.get("selected_tools"),
|
|
399
|
+
selected_prompts=data.get("selected_prompts"),
|
|
400
|
+
selected_data_sources=data.get("selected_data_sources"),
|
|
401
|
+
only_rag=data.get("only_rag", False),
|
|
402
|
+
tool_choice_required=data.get("tool_choice_required", False),
|
|
403
|
+
user_email=user_email, # Use authenticated user from connection
|
|
404
|
+
agent_mode=data.get("agent_mode", False),
|
|
405
|
+
agent_max_steps=data.get("agent_max_steps", 10),
|
|
406
|
+
temperature=data.get("temperature", 0.7),
|
|
407
|
+
agent_loop_strategy=data.get("agent_loop_strategy"),
|
|
408
|
+
update_callback=lambda message: websocket_update_callback(websocket, message),
|
|
409
|
+
files=data.get("files")
|
|
410
|
+
)
|
|
411
|
+
except RateLimitError as e:
|
|
412
|
+
logger.warning(f"Rate limit error in chat handler: {e}")
|
|
413
|
+
log_metric("error", user_email, error_type="rate_limit")
|
|
414
|
+
await websocket.send_json({
|
|
415
|
+
"type": "error",
|
|
416
|
+
"message": str(e.message if hasattr(e, 'message') else e),
|
|
417
|
+
"error_type": "rate_limit"
|
|
418
|
+
})
|
|
419
|
+
except LLMTimeoutError as e:
|
|
420
|
+
logger.warning(f"Timeout error in chat handler: {e}")
|
|
421
|
+
log_metric("error", user_email, error_type="timeout")
|
|
422
|
+
await websocket.send_json({
|
|
423
|
+
"type": "error",
|
|
424
|
+
"message": str(e.message if hasattr(e, 'message') else e),
|
|
425
|
+
"error_type": "timeout"
|
|
426
|
+
})
|
|
427
|
+
except LLMAuthenticationError as e:
|
|
428
|
+
logger.error(f"Authentication error in chat handler: {e}")
|
|
429
|
+
log_metric("error", user_email, error_type="authentication")
|
|
430
|
+
await websocket.send_json({
|
|
431
|
+
"type": "error",
|
|
432
|
+
"message": str(e.message if hasattr(e, 'message') else e),
|
|
433
|
+
"error_type": "authentication"
|
|
434
|
+
})
|
|
435
|
+
except ValidationError as e:
|
|
436
|
+
logger.warning(f"Validation error in chat handler: {e}")
|
|
437
|
+
log_metric("error", user_email, error_type="validation")
|
|
438
|
+
await websocket.send_json({
|
|
439
|
+
"type": "error",
|
|
440
|
+
"message": str(e.message if hasattr(e, 'message') else e),
|
|
441
|
+
"error_type": "validation"
|
|
442
|
+
})
|
|
443
|
+
except DomainError as e:
|
|
444
|
+
logger.error(f"Domain error in chat handler: {e}", exc_info=True)
|
|
445
|
+
log_metric("error", user_email, error_type="domain")
|
|
446
|
+
await websocket.send_json({
|
|
447
|
+
"type": "error",
|
|
448
|
+
"message": str(e.message if hasattr(e, 'message') else e),
|
|
449
|
+
"error_type": "domain"
|
|
450
|
+
})
|
|
451
|
+
except Exception as e:
|
|
452
|
+
logger.error(f"Unexpected error in chat handler: {e}", exc_info=True)
|
|
453
|
+
log_metric("error", user_email, error_type="unexpected")
|
|
454
|
+
await websocket.send_json({
|
|
455
|
+
"type": "error",
|
|
456
|
+
"message": "An unexpected error occurred. Please try again or contact support if the issue persists.",
|
|
457
|
+
"error_type": "unexpected"
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
# Start chat handling in background
|
|
461
|
+
asyncio.create_task(handle_chat())
|
|
462
|
+
|
|
463
|
+
elif message_type == "download_file":
|
|
464
|
+
# Handle file download (use authenticated user from connection)
|
|
465
|
+
response = await chat_service.handle_download_file(
|
|
466
|
+
session_id=session_id,
|
|
467
|
+
filename=data.get("filename", ""),
|
|
468
|
+
user_email=user_email
|
|
469
|
+
)
|
|
470
|
+
await websocket.send_json(response)
|
|
471
|
+
|
|
472
|
+
elif message_type == "reset_session":
|
|
473
|
+
# Handle session reset (use authenticated user from connection)
|
|
474
|
+
response = await chat_service.handle_reset_session(
|
|
475
|
+
session_id=session_id,
|
|
476
|
+
user_email=user_email
|
|
477
|
+
)
|
|
478
|
+
await websocket.send_json(response)
|
|
479
|
+
|
|
480
|
+
elif message_type == "attach_file":
|
|
481
|
+
# Handle file attachment to session (use authenticated user, not client-sent)
|
|
482
|
+
response = await chat_service.handle_attach_file(
|
|
483
|
+
session_id=session_id,
|
|
484
|
+
s3_key=data.get("s3_key"),
|
|
485
|
+
user_email=user_email, # Use authenticated user from connection
|
|
486
|
+
update_callback=lambda message: websocket_update_callback(websocket, message)
|
|
487
|
+
)
|
|
488
|
+
await websocket.send_json(response)
|
|
489
|
+
|
|
490
|
+
elif message_type == "tool_approval_response":
|
|
491
|
+
# Handle tool approval response
|
|
492
|
+
from atlas.application.chat.approval_manager import get_approval_manager
|
|
493
|
+
approval_manager = get_approval_manager()
|
|
494
|
+
|
|
495
|
+
tool_call_id = data.get("tool_call_id")
|
|
496
|
+
approved = data.get("approved", False)
|
|
497
|
+
arguments = data.get("arguments")
|
|
498
|
+
reason = data.get("reason")
|
|
499
|
+
|
|
500
|
+
# SECURITY: Never log tool arguments at INFO level (they may include sensitive user data).
|
|
501
|
+
# Log a conservative summary instead.
|
|
502
|
+
logger.info(
|
|
503
|
+
"Received tool approval response: %s",
|
|
504
|
+
summarize_tool_approval_response_for_logging(data),
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
logger.info(f"Processing approval: tool_call_id={sanitize_for_logging(tool_call_id)}, approved={approved}")
|
|
508
|
+
|
|
509
|
+
result = approval_manager.handle_approval_response(
|
|
510
|
+
tool_call_id=tool_call_id,
|
|
511
|
+
approved=approved,
|
|
512
|
+
arguments=arguments,
|
|
513
|
+
reason=reason
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
logger.info(f"Approval response handled: result={sanitize_for_logging(result)}")
|
|
517
|
+
# No response needed - the approval will unblock the waiting tool execution
|
|
518
|
+
|
|
519
|
+
elif message_type == "elicitation_response":
|
|
520
|
+
# Handle elicitation response
|
|
521
|
+
from atlas.application.chat.elicitation_manager import get_elicitation_manager
|
|
522
|
+
elicitation_manager = get_elicitation_manager()
|
|
523
|
+
|
|
524
|
+
elicitation_id = data.get("elicitation_id")
|
|
525
|
+
action = data.get("action", "cancel")
|
|
526
|
+
response_data = data.get("data")
|
|
527
|
+
|
|
528
|
+
logger.info(
|
|
529
|
+
f"Received elicitation response: id={sanitize_for_logging(elicitation_id)}, "
|
|
530
|
+
f"action={action}"
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
result = elicitation_manager.handle_elicitation_response(
|
|
534
|
+
elicitation_id=elicitation_id,
|
|
535
|
+
action=action,
|
|
536
|
+
data=response_data
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
logger.info(f"Elicitation response handled: result={sanitize_for_logging(result)}")
|
|
540
|
+
# No response needed - the elicitation will unblock the waiting tool execution
|
|
541
|
+
|
|
542
|
+
else:
|
|
543
|
+
logger.warning(f"Unknown message type: {sanitize_for_logging(message_type)}")
|
|
544
|
+
await websocket.send_json({
|
|
545
|
+
"type": "error",
|
|
546
|
+
"message": f"Unknown message type: {sanitize_for_logging(message_type)}"
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
except WebSocketDisconnect:
|
|
550
|
+
chat_service.end_session(session_id)
|
|
551
|
+
logger.info(f"WebSocket connection closed for session {session_id}")
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
if __name__ == "__main__":
|
|
555
|
+
import os
|
|
556
|
+
|
|
557
|
+
import uvicorn
|
|
558
|
+
|
|
559
|
+
# Use environment variable for host binding, default to localhost for security
|
|
560
|
+
# Set ATLAS_HOST=0.0.0.0 in production environments where needed
|
|
561
|
+
host = os.getenv("ATLAS_HOST", "127.0.0.1")
|
|
562
|
+
port = int(os.getenv("PORT", 8000))
|
|
563
|
+
|
|
564
|
+
uvicorn.run(app, host=host, port=port)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# API Key Demo MCP Server
|
|
2
|
+
|
|
3
|
+
Last updated: 2026-01-25
|
|
4
|
+
|
|
5
|
+
This MCP server demonstrates per-user API key authentication. It validates the `X-API-Key` header on all tool calls using FastMCP middleware.
|
|
6
|
+
|
|
7
|
+
## Running the Server
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# From this directory
|
|
11
|
+
./run.sh
|
|
12
|
+
|
|
13
|
+
# Or with custom port
|
|
14
|
+
./run.sh 9000
|
|
15
|
+
|
|
16
|
+
# Or directly with Python
|
|
17
|
+
python main.py
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Valid Test Keys
|
|
21
|
+
|
|
22
|
+
For demo purposes, these API keys are accepted:
|
|
23
|
+
- `test123` (developer)
|
|
24
|
+
- `admin123` (admin)
|
|
25
|
+
- `demo-api-key-12345` (viewer)
|
|
26
|
+
|
|
27
|
+
## Configuration in Atlas
|
|
28
|
+
|
|
29
|
+
Add to `config/overrides/mcp.json`:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"api_key_demo": {
|
|
34
|
+
"url": "http://127.0.0.1:8006/mcp",
|
|
35
|
+
"transport": "http",
|
|
36
|
+
"groups": ["users"],
|
|
37
|
+
"description": "API key authentication demo",
|
|
38
|
+
"auth_type": "api_key",
|
|
39
|
+
"auth_header": "X-API-Key",
|
|
40
|
+
"auth_prompt": "Enter your API key for the demo server"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## How It Works
|
|
46
|
+
|
|
47
|
+
1. **Server Side**: The `ApiKeyAuthMiddleware` class intercepts all tool calls and validates the `X-API-Key` header against a set of valid keys.
|
|
48
|
+
|
|
49
|
+
2. **Client Side**: Atlas UI detects `auth_type: "api_key"` in the config and prompts users to enter their API key via the TokenInputModal.
|
|
50
|
+
|
|
51
|
+
3. **Storage**: User API keys are stored encrypted in `config/secure/mcp_tokens.enc` per-user per-server.
|
|
52
|
+
|
|
53
|
+
4. **Injection**: When calling tools, Atlas creates a per-user MCP client with the API key injected via `StreamableHttpTransport(headers={"X-API-Key": key})`.
|
|
54
|
+
|
|
55
|
+
## Available Tools
|
|
56
|
+
|
|
57
|
+
- `echo(message)` - Echo back a message
|
|
58
|
+
- `add_numbers(a, b)` - Add two numbers
|
|
59
|
+
- `get_user_data()` - Get sample protected data
|
|
60
|
+
- `list_valid_keys()` - List valid demo keys (for testing)
|
|
61
|
+
|
|
62
|
+
## Testing with FastMCP Client
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from fastmcp import Client
|
|
66
|
+
from fastmcp.client.transports import StreamableHttpTransport
|
|
67
|
+
|
|
68
|
+
transport = StreamableHttpTransport(
|
|
69
|
+
"http://localhost:8006/mcp",
|
|
70
|
+
headers={"X-API-Key": "demo-api-key-12345"}
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
async with Client(transport=transport) as client:
|
|
74
|
+
result = await client.call_tool("echo", {"message": "Hello!"})
|
|
75
|
+
print(result)
|
|
76
|
+
```
|