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.
Files changed (250) hide show
  1. atlas/__init__.py +40 -0
  2. atlas/application/__init__.py +7 -0
  3. atlas/application/chat/__init__.py +7 -0
  4. atlas/application/chat/agent/__init__.py +10 -0
  5. atlas/application/chat/agent/act_loop.py +179 -0
  6. atlas/application/chat/agent/factory.py +142 -0
  7. atlas/application/chat/agent/protocols.py +46 -0
  8. atlas/application/chat/agent/react_loop.py +338 -0
  9. atlas/application/chat/agent/think_act_loop.py +171 -0
  10. atlas/application/chat/approval_manager.py +151 -0
  11. atlas/application/chat/elicitation_manager.py +191 -0
  12. atlas/application/chat/events/__init__.py +1 -0
  13. atlas/application/chat/events/agent_event_relay.py +112 -0
  14. atlas/application/chat/modes/__init__.py +1 -0
  15. atlas/application/chat/modes/agent.py +125 -0
  16. atlas/application/chat/modes/plain.py +74 -0
  17. atlas/application/chat/modes/rag.py +81 -0
  18. atlas/application/chat/modes/tools.py +179 -0
  19. atlas/application/chat/orchestrator.py +213 -0
  20. atlas/application/chat/policies/__init__.py +1 -0
  21. atlas/application/chat/policies/tool_authorization.py +99 -0
  22. atlas/application/chat/preprocessors/__init__.py +1 -0
  23. atlas/application/chat/preprocessors/message_builder.py +92 -0
  24. atlas/application/chat/preprocessors/prompt_override_service.py +104 -0
  25. atlas/application/chat/service.py +454 -0
  26. atlas/application/chat/utilities/__init__.py +6 -0
  27. atlas/application/chat/utilities/error_handler.py +367 -0
  28. atlas/application/chat/utilities/event_notifier.py +546 -0
  29. atlas/application/chat/utilities/file_processor.py +613 -0
  30. atlas/application/chat/utilities/tool_executor.py +789 -0
  31. atlas/atlas_chat_cli.py +347 -0
  32. atlas/atlas_client.py +238 -0
  33. atlas/core/__init__.py +0 -0
  34. atlas/core/auth.py +205 -0
  35. atlas/core/authorization_manager.py +27 -0
  36. atlas/core/capabilities.py +123 -0
  37. atlas/core/compliance.py +215 -0
  38. atlas/core/domain_whitelist.py +147 -0
  39. atlas/core/domain_whitelist_middleware.py +82 -0
  40. atlas/core/http_client.py +28 -0
  41. atlas/core/log_sanitizer.py +102 -0
  42. atlas/core/metrics_logger.py +59 -0
  43. atlas/core/middleware.py +131 -0
  44. atlas/core/otel_config.py +242 -0
  45. atlas/core/prompt_risk.py +200 -0
  46. atlas/core/rate_limit.py +0 -0
  47. atlas/core/rate_limit_middleware.py +64 -0
  48. atlas/core/security_headers_middleware.py +51 -0
  49. atlas/domain/__init__.py +37 -0
  50. atlas/domain/chat/__init__.py +1 -0
  51. atlas/domain/chat/dtos.py +85 -0
  52. atlas/domain/errors.py +96 -0
  53. atlas/domain/messages/__init__.py +12 -0
  54. atlas/domain/messages/models.py +160 -0
  55. atlas/domain/rag_mcp_service.py +664 -0
  56. atlas/domain/sessions/__init__.py +7 -0
  57. atlas/domain/sessions/models.py +36 -0
  58. atlas/domain/unified_rag_service.py +371 -0
  59. atlas/infrastructure/__init__.py +10 -0
  60. atlas/infrastructure/app_factory.py +135 -0
  61. atlas/infrastructure/events/__init__.py +1 -0
  62. atlas/infrastructure/events/cli_event_publisher.py +140 -0
  63. atlas/infrastructure/events/websocket_publisher.py +140 -0
  64. atlas/infrastructure/sessions/in_memory_repository.py +56 -0
  65. atlas/infrastructure/transport/__init__.py +7 -0
  66. atlas/infrastructure/transport/websocket_connection_adapter.py +33 -0
  67. atlas/init_cli.py +226 -0
  68. atlas/interfaces/__init__.py +15 -0
  69. atlas/interfaces/events.py +134 -0
  70. atlas/interfaces/llm.py +54 -0
  71. atlas/interfaces/rag.py +40 -0
  72. atlas/interfaces/sessions.py +75 -0
  73. atlas/interfaces/tools.py +57 -0
  74. atlas/interfaces/transport.py +24 -0
  75. atlas/main.py +564 -0
  76. atlas/mcp/api_key_demo/README.md +76 -0
  77. atlas/mcp/api_key_demo/main.py +172 -0
  78. atlas/mcp/api_key_demo/run.sh +56 -0
  79. atlas/mcp/basictable/main.py +147 -0
  80. atlas/mcp/calculator/main.py +149 -0
  81. atlas/mcp/code-executor/execution_engine.py +98 -0
  82. atlas/mcp/code-executor/execution_environment.py +95 -0
  83. atlas/mcp/code-executor/main.py +528 -0
  84. atlas/mcp/code-executor/result_processing.py +276 -0
  85. atlas/mcp/code-executor/script_generation.py +195 -0
  86. atlas/mcp/code-executor/security_checker.py +140 -0
  87. atlas/mcp/corporate_cars/main.py +437 -0
  88. atlas/mcp/csv_reporter/main.py +545 -0
  89. atlas/mcp/duckduckgo/main.py +182 -0
  90. atlas/mcp/elicitation_demo/README.md +171 -0
  91. atlas/mcp/elicitation_demo/main.py +262 -0
  92. atlas/mcp/env-demo/README.md +158 -0
  93. atlas/mcp/env-demo/main.py +199 -0
  94. atlas/mcp/file_size_test/main.py +284 -0
  95. atlas/mcp/filesystem/main.py +348 -0
  96. atlas/mcp/image_demo/main.py +113 -0
  97. atlas/mcp/image_demo/requirements.txt +4 -0
  98. atlas/mcp/logging_demo/README.md +72 -0
  99. atlas/mcp/logging_demo/main.py +103 -0
  100. atlas/mcp/many_tools_demo/main.py +50 -0
  101. atlas/mcp/order_database/__init__.py +0 -0
  102. atlas/mcp/order_database/main.py +369 -0
  103. atlas/mcp/order_database/signal_data.csv +1001 -0
  104. atlas/mcp/pdfbasic/main.py +394 -0
  105. atlas/mcp/pptx_generator/main.py +760 -0
  106. atlas/mcp/pptx_generator/requirements.txt +13 -0
  107. atlas/mcp/pptx_generator/run_test.sh +1 -0
  108. atlas/mcp/pptx_generator/test_pptx_generator_security.py +169 -0
  109. atlas/mcp/progress_demo/main.py +167 -0
  110. atlas/mcp/progress_updates_demo/QUICKSTART.md +273 -0
  111. atlas/mcp/progress_updates_demo/README.md +120 -0
  112. atlas/mcp/progress_updates_demo/main.py +497 -0
  113. atlas/mcp/prompts/main.py +222 -0
  114. atlas/mcp/public_demo/main.py +189 -0
  115. atlas/mcp/sampling_demo/README.md +169 -0
  116. atlas/mcp/sampling_demo/main.py +234 -0
  117. atlas/mcp/thinking/main.py +77 -0
  118. atlas/mcp/tool_planner/main.py +240 -0
  119. atlas/mcp/ui-demo/badmesh.png +0 -0
  120. atlas/mcp/ui-demo/main.py +383 -0
  121. atlas/mcp/ui-demo/templates/button_demo.html +32 -0
  122. atlas/mcp/ui-demo/templates/data_visualization.html +32 -0
  123. atlas/mcp/ui-demo/templates/form_demo.html +28 -0
  124. atlas/mcp/username-override-demo/README.md +320 -0
  125. atlas/mcp/username-override-demo/main.py +308 -0
  126. atlas/modules/__init__.py +0 -0
  127. atlas/modules/config/__init__.py +34 -0
  128. atlas/modules/config/cli.py +231 -0
  129. atlas/modules/config/config_manager.py +1096 -0
  130. atlas/modules/file_storage/__init__.py +22 -0
  131. atlas/modules/file_storage/cli.py +330 -0
  132. atlas/modules/file_storage/content_extractor.py +290 -0
  133. atlas/modules/file_storage/manager.py +295 -0
  134. atlas/modules/file_storage/mock_s3_client.py +402 -0
  135. atlas/modules/file_storage/s3_client.py +417 -0
  136. atlas/modules/llm/__init__.py +19 -0
  137. atlas/modules/llm/caller.py +287 -0
  138. atlas/modules/llm/litellm_caller.py +675 -0
  139. atlas/modules/llm/models.py +19 -0
  140. atlas/modules/mcp_tools/__init__.py +17 -0
  141. atlas/modules/mcp_tools/client.py +2123 -0
  142. atlas/modules/mcp_tools/token_storage.py +556 -0
  143. atlas/modules/prompts/prompt_provider.py +130 -0
  144. atlas/modules/rag/__init__.py +24 -0
  145. atlas/modules/rag/atlas_rag_client.py +336 -0
  146. atlas/modules/rag/client.py +129 -0
  147. atlas/routes/admin_routes.py +865 -0
  148. atlas/routes/config_routes.py +484 -0
  149. atlas/routes/feedback_routes.py +361 -0
  150. atlas/routes/files_routes.py +274 -0
  151. atlas/routes/health_routes.py +40 -0
  152. atlas/routes/mcp_auth_routes.py +223 -0
  153. atlas/server_cli.py +164 -0
  154. atlas/tests/conftest.py +20 -0
  155. atlas/tests/integration/test_mcp_auth_integration.py +152 -0
  156. atlas/tests/manual_test_sampling.py +87 -0
  157. atlas/tests/modules/mcp_tools/test_client_auth.py +226 -0
  158. atlas/tests/modules/mcp_tools/test_client_env.py +191 -0
  159. atlas/tests/test_admin_mcp_server_management_routes.py +141 -0
  160. atlas/tests/test_agent_roa.py +135 -0
  161. atlas/tests/test_app_factory_smoke.py +47 -0
  162. atlas/tests/test_approval_manager.py +439 -0
  163. atlas/tests/test_atlas_client.py +188 -0
  164. atlas/tests/test_atlas_rag_client.py +447 -0
  165. atlas/tests/test_atlas_rag_integration.py +224 -0
  166. atlas/tests/test_attach_file_flow.py +287 -0
  167. atlas/tests/test_auth_utils.py +165 -0
  168. atlas/tests/test_backend_public_url.py +185 -0
  169. atlas/tests/test_banner_logging.py +287 -0
  170. atlas/tests/test_capability_tokens_and_injection.py +203 -0
  171. atlas/tests/test_compliance_level.py +54 -0
  172. atlas/tests/test_compliance_manager.py +253 -0
  173. atlas/tests/test_config_manager.py +617 -0
  174. atlas/tests/test_config_manager_paths.py +12 -0
  175. atlas/tests/test_core_auth.py +18 -0
  176. atlas/tests/test_core_utils.py +190 -0
  177. atlas/tests/test_docker_env_sync.py +202 -0
  178. atlas/tests/test_domain_errors.py +329 -0
  179. atlas/tests/test_domain_whitelist.py +359 -0
  180. atlas/tests/test_elicitation_manager.py +408 -0
  181. atlas/tests/test_elicitation_routing.py +296 -0
  182. atlas/tests/test_env_demo_server.py +88 -0
  183. atlas/tests/test_error_classification.py +113 -0
  184. atlas/tests/test_error_flow_integration.py +116 -0
  185. atlas/tests/test_feedback_routes.py +333 -0
  186. atlas/tests/test_file_content_extraction.py +1134 -0
  187. atlas/tests/test_file_extraction_routes.py +158 -0
  188. atlas/tests/test_file_library.py +107 -0
  189. atlas/tests/test_file_manager_unit.py +18 -0
  190. atlas/tests/test_health_route.py +49 -0
  191. atlas/tests/test_http_client_stub.py +8 -0
  192. atlas/tests/test_imports_smoke.py +30 -0
  193. atlas/tests/test_interfaces_llm_response.py +9 -0
  194. atlas/tests/test_issue_access_denied_fix.py +136 -0
  195. atlas/tests/test_llm_env_expansion.py +836 -0
  196. atlas/tests/test_log_level_sensitive_data.py +285 -0
  197. atlas/tests/test_mcp_auth_routes.py +341 -0
  198. atlas/tests/test_mcp_client_auth.py +331 -0
  199. atlas/tests/test_mcp_data_injection.py +270 -0
  200. atlas/tests/test_mcp_get_authorized_servers.py +95 -0
  201. atlas/tests/test_mcp_hot_reload.py +512 -0
  202. atlas/tests/test_mcp_image_content.py +424 -0
  203. atlas/tests/test_mcp_logging.py +172 -0
  204. atlas/tests/test_mcp_progress_updates.py +313 -0
  205. atlas/tests/test_mcp_prompt_override_system_prompt.py +102 -0
  206. atlas/tests/test_mcp_prompts_server.py +39 -0
  207. atlas/tests/test_mcp_tool_result_parsing.py +296 -0
  208. atlas/tests/test_metrics_logger.py +56 -0
  209. atlas/tests/test_middleware_auth.py +379 -0
  210. atlas/tests/test_prompt_risk_and_acl.py +141 -0
  211. atlas/tests/test_rag_mcp_aggregator.py +204 -0
  212. atlas/tests/test_rag_mcp_service.py +224 -0
  213. atlas/tests/test_rate_limit_middleware.py +45 -0
  214. atlas/tests/test_routes_config_smoke.py +60 -0
  215. atlas/tests/test_routes_files_download_token.py +41 -0
  216. atlas/tests/test_routes_files_health.py +18 -0
  217. atlas/tests/test_runtime_imports.py +53 -0
  218. atlas/tests/test_sampling_integration.py +482 -0
  219. atlas/tests/test_security_admin_routes.py +61 -0
  220. atlas/tests/test_security_capability_tokens.py +65 -0
  221. atlas/tests/test_security_file_stats_scope.py +21 -0
  222. atlas/tests/test_security_header_injection.py +191 -0
  223. atlas/tests/test_security_headers_and_filename.py +63 -0
  224. atlas/tests/test_shared_session_repository.py +101 -0
  225. atlas/tests/test_system_prompt_loading.py +181 -0
  226. atlas/tests/test_token_storage.py +505 -0
  227. atlas/tests/test_tool_approval_config.py +93 -0
  228. atlas/tests/test_tool_approval_utils.py +356 -0
  229. atlas/tests/test_tool_authorization_group_filtering.py +223 -0
  230. atlas/tests/test_tool_details_in_config.py +108 -0
  231. atlas/tests/test_tool_planner.py +300 -0
  232. atlas/tests/test_unified_rag_service.py +398 -0
  233. atlas/tests/test_username_override_in_approval.py +258 -0
  234. atlas/tests/test_websocket_auth_header.py +168 -0
  235. atlas/version.py +6 -0
  236. atlas_chat-0.1.0.data/data/.env.example +253 -0
  237. atlas_chat-0.1.0.data/data/config/defaults/compliance-levels.json +44 -0
  238. atlas_chat-0.1.0.data/data/config/defaults/domain-whitelist.json +123 -0
  239. atlas_chat-0.1.0.data/data/config/defaults/file-extractors.json +74 -0
  240. atlas_chat-0.1.0.data/data/config/defaults/help-config.json +198 -0
  241. atlas_chat-0.1.0.data/data/config/defaults/llmconfig-buggy.yml +11 -0
  242. atlas_chat-0.1.0.data/data/config/defaults/llmconfig.yml +19 -0
  243. atlas_chat-0.1.0.data/data/config/defaults/mcp.json +138 -0
  244. atlas_chat-0.1.0.data/data/config/defaults/rag-sources.json +17 -0
  245. atlas_chat-0.1.0.data/data/config/defaults/splash-config.json +16 -0
  246. atlas_chat-0.1.0.dist-info/METADATA +236 -0
  247. atlas_chat-0.1.0.dist-info/RECORD +250 -0
  248. atlas_chat-0.1.0.dist-info/WHEEL +5 -0
  249. atlas_chat-0.1.0.dist-info/entry_points.txt +4 -0
  250. atlas_chat-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,223 @@
1
+ """MCP Authentication routes for per-user API key and token management.
2
+
3
+ This module provides user-facing endpoints for managing authentication tokens
4
+ with MCP servers. Each user's tokens are stored separately and securely encrypted.
5
+
6
+ Users can manually upload API keys, JWTs, or bearer tokens for MCP servers that
7
+ require authentication. This supports any type of token that can be passed as a
8
+ bearer token in the Authorization header.
9
+
10
+ Updated: 2025-01-21
11
+ """
12
+
13
+ import logging
14
+ from typing import Optional
15
+
16
+ from fastapi import APIRouter, Depends, HTTPException
17
+ from pydantic import BaseModel
18
+
19
+ from atlas.core.auth import is_user_in_group
20
+ from atlas.core.log_sanitizer import get_current_user, sanitize_for_logging
21
+ from atlas.infrastructure.app_factory import app_factory
22
+ from atlas.modules.mcp_tools.token_storage import get_token_storage
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ router = APIRouter(prefix="/api/mcp/auth", tags=["mcp-auth"])
27
+
28
+
29
+ class TokenUpload(BaseModel):
30
+ """Request body for uploading an API key or token."""
31
+ token: str
32
+ expires_at: Optional[float] = None # Unix timestamp, or None for no expiry
33
+ scopes: Optional[str] = None # Space-separated scopes (optional)
34
+
35
+
36
+ # --- Routes ---
37
+
38
+
39
+ @router.get("/status")
40
+ async def get_auth_status(current_user: str = Depends(get_current_user)):
41
+ """Get authentication status for all MCP servers accessible to the user.
42
+
43
+ Returns information about which servers require authentication,
44
+ which the user has authenticated with, and token status.
45
+ """
46
+ try:
47
+ mcp_manager = app_factory.get_mcp_manager()
48
+ token_storage = get_token_storage()
49
+
50
+ # Get servers the user is authorized to access
51
+ authorized_servers = await mcp_manager.get_authorized_servers(
52
+ current_user, is_user_in_group
53
+ )
54
+
55
+ # Get user's current auth status
56
+ user_auth_status = token_storage.get_user_auth_status(current_user)
57
+
58
+ # Build response with server auth requirements and user's status
59
+ servers_status = []
60
+
61
+ for server_name in authorized_servers:
62
+ server_config = mcp_manager.servers_config.get(server_name, {})
63
+ auth_type = server_config.get("auth_type", "none")
64
+
65
+ # Get user's token status for this server
66
+ token_status = user_auth_status.get(server_name)
67
+
68
+ server_info = {
69
+ "server_name": server_name,
70
+ "auth_type": auth_type,
71
+ "auth_required": auth_type != "none",
72
+ "authenticated": token_status is not None,
73
+ "description": server_config.get("description", ""),
74
+ }
75
+
76
+ # Add token details if authenticated
77
+ if token_status:
78
+ server_info.update({
79
+ "token_type": token_status["token_type"],
80
+ "is_expired": token_status["is_expired"],
81
+ "expires_at": token_status["expires_at"],
82
+ "time_until_expiry": token_status["time_until_expiry"],
83
+ "has_refresh_token": token_status["has_refresh_token"],
84
+ "scopes": token_status["scopes"],
85
+ })
86
+
87
+ servers_status.append(server_info)
88
+
89
+ return {
90
+ "servers": servers_status,
91
+ "user": current_user,
92
+ }
93
+
94
+ except Exception as e:
95
+ logger.error(f"Error getting auth status: {e}", exc_info=True)
96
+ raise HTTPException(status_code=500, detail="Internal server error while fetching auth status")
97
+
98
+
99
+ @router.post("/{server_name}/token")
100
+ async def upload_token(
101
+ server_name: str,
102
+ token_data: TokenUpload,
103
+ current_user: str = Depends(get_current_user),
104
+ ):
105
+ """Upload an API key or bearer token for an MCP server.
106
+
107
+ This allows users to manually provide tokens for servers that require
108
+ authentication. Tokens can be:
109
+ - API keys
110
+ - JWT tokens
111
+ - Bearer tokens
112
+ - Any other string token that can be used in Authorization header
113
+
114
+ The token will be securely encrypted and stored per-user.
115
+ """
116
+ try:
117
+ mcp_manager = app_factory.get_mcp_manager()
118
+ token_storage = get_token_storage()
119
+
120
+ # Verify server exists and user has access
121
+ authorized_servers = await mcp_manager.get_authorized_servers(
122
+ current_user, is_user_in_group
123
+ )
124
+
125
+ if server_name not in authorized_servers:
126
+ raise HTTPException(
127
+ status_code=403,
128
+ detail=f"Not authorized to access server '{server_name}'"
129
+ )
130
+
131
+ # Verify server accepts token authentication
132
+ server_config = mcp_manager.servers_config.get(server_name, {})
133
+ auth_type = server_config.get("auth_type", "none")
134
+
135
+ if auth_type not in ("jwt", "bearer", "api_key", "oauth"):
136
+ raise HTTPException(
137
+ status_code=400,
138
+ detail=f"Server '{server_name}' does not accept token authentication (auth_type: {auth_type})"
139
+ )
140
+
141
+ # Validate token is not empty
142
+ if not token_data.token or not token_data.token.strip():
143
+ raise HTTPException(
144
+ status_code=400,
145
+ detail="Token cannot be empty"
146
+ )
147
+
148
+ # Determine token type based on server config
149
+ token_type = "bearer"
150
+ if auth_type == "jwt":
151
+ token_type = "jwt"
152
+ elif auth_type == "api_key":
153
+ token_type = "api_key"
154
+
155
+ # Store the token
156
+ stored_token = token_storage.store_token(
157
+ user_email=current_user,
158
+ server_name=server_name,
159
+ token_value=token_data.token.strip(),
160
+ token_type=token_type,
161
+ expires_at=token_data.expires_at,
162
+ scopes=token_data.scopes,
163
+ )
164
+
165
+ sanitized_server = sanitize_for_logging(server_name)
166
+ logger.info(f"User uploaded token for MCP server '{sanitized_server}'")
167
+
168
+ return {
169
+ "message": f"Token stored for server '{server_name}'",
170
+ "server_name": server_name,
171
+ "token_type": stored_token.token_type,
172
+ "expires_at": stored_token.expires_at,
173
+ "scopes": stored_token.scopes,
174
+ }
175
+
176
+ except HTTPException:
177
+ raise
178
+ except Exception as e:
179
+ logger.error(f"Error uploading token: {e}", exc_info=True)
180
+ raise HTTPException(status_code=500, detail="Internal server error while uploading token")
181
+
182
+
183
+ @router.delete("/{server_name}/token")
184
+ async def remove_token(
185
+ server_name: str,
186
+ current_user: str = Depends(get_current_user),
187
+ ):
188
+ """Remove stored token for an MCP server (disconnect).
189
+
190
+ This removes the user's authentication token for the specified server.
191
+ The user will need to re-authenticate to use the server's tools.
192
+ """
193
+ try:
194
+ token_storage = get_token_storage()
195
+
196
+ # Remove the token
197
+ removed = token_storage.remove_token(current_user, server_name)
198
+
199
+ if not removed:
200
+ raise HTTPException(
201
+ status_code=404,
202
+ detail=f"No token found for server '{server_name}'"
203
+ )
204
+
205
+ # Invalidate any cached client for this user/server combination
206
+ tool_manager = app_factory.get_mcp_manager()
207
+ if tool_manager is not None:
208
+ await tool_manager._invalidate_user_client(current_user, server_name)
209
+ logger.debug(f"Invalidated cached client for server '{server_name}'")
210
+
211
+ sanitized_server = sanitize_for_logging(server_name)
212
+ logger.info(f"User removed token for MCP server '{sanitized_server}'")
213
+
214
+ return {
215
+ "message": f"Token removed for server '{server_name}'",
216
+ "server_name": server_name,
217
+ }
218
+
219
+ except HTTPException:
220
+ raise
221
+ except Exception as e:
222
+ logger.error(f"Error removing token: {e}", exc_info=True)
223
+ raise HTTPException(status_code=500, detail="Internal server error while removing token")
atlas/server_cli.py ADDED
@@ -0,0 +1,164 @@
1
+ """
2
+ Atlas Server CLI - Start the Atlas backend server.
3
+
4
+ Usage:
5
+ atlas-server # Start with defaults
6
+ atlas-server --port 8000 # Custom port
7
+ atlas-server --env /path/to/.env # Custom env file
8
+ atlas-server --config-folder /path/to/config # Custom config folder
9
+ """
10
+
11
+ import argparse
12
+ import os
13
+ import sys
14
+ from pathlib import Path
15
+
16
+
17
+ def _apply_env_file(env_path: Path) -> None:
18
+ """Load environment variables from a .env file."""
19
+ from dotenv import load_dotenv
20
+
21
+ if not env_path.exists():
22
+ print(f"Error: env file not found: {env_path}", file=sys.stderr)
23
+ sys.exit(2)
24
+
25
+ load_dotenv(dotenv_path=str(env_path))
26
+
27
+
28
+ def _apply_config_folder(config_folder: Path) -> None:
29
+ """Set up config folder as the overrides directory."""
30
+ if not config_folder.exists():
31
+ print(f"Error: config folder not found: {config_folder}", file=sys.stderr)
32
+ sys.exit(2)
33
+
34
+ os.environ["APP_CONFIG_OVERRIDES"] = str(config_folder.resolve())
35
+
36
+
37
+ def build_parser() -> argparse.ArgumentParser:
38
+ """Build argument parser for atlas-server CLI."""
39
+ parser = argparse.ArgumentParser(
40
+ prog="atlas-server",
41
+ description="Start the Atlas backend server with MCP integration.",
42
+ )
43
+ parser.add_argument(
44
+ "--port",
45
+ type=int,
46
+ default=None,
47
+ help="Port to run the server on (default: 8000 or PORT env var).",
48
+ )
49
+ parser.add_argument(
50
+ "--host",
51
+ default=None,
52
+ help="Host to bind to (default: 127.0.0.1 or ATLAS_HOST env var).",
53
+ )
54
+ parser.add_argument(
55
+ "--env",
56
+ dest="env_file",
57
+ default=None,
58
+ help="Path to .env file (default: .env in current directory or package root).",
59
+ )
60
+ parser.add_argument(
61
+ "--config-folder",
62
+ dest="config_folder",
63
+ default=None,
64
+ help="Path to config folder for overrides (sets APP_CONFIG_OVERRIDES).",
65
+ )
66
+ parser.add_argument(
67
+ "--config-defaults",
68
+ dest="config_defaults",
69
+ default=None,
70
+ help="Path to config defaults folder (sets APP_CONFIG_DEFAULTS).",
71
+ )
72
+ parser.add_argument(
73
+ "--reload",
74
+ action="store_true",
75
+ help="Enable auto-reload for development (not recommended for production).",
76
+ )
77
+ parser.add_argument(
78
+ "--workers",
79
+ type=int,
80
+ default=1,
81
+ help="Number of worker processes (default: 1).",
82
+ )
83
+ parser.add_argument(
84
+ "--version",
85
+ action="store_true",
86
+ help="Print version and exit.",
87
+ )
88
+ return parser
89
+
90
+
91
+ def run_server(args: argparse.Namespace) -> int:
92
+ """Run the Atlas server with the given arguments."""
93
+ import uvicorn
94
+
95
+ # Determine host and port
96
+ host = args.host or os.getenv("ATLAS_HOST", "127.0.0.1")
97
+ port = args.port or int(os.getenv("PORT", "8000"))
98
+
99
+ # Import the FastAPI app
100
+ from atlas.main import app
101
+
102
+ print(f"Starting Atlas server on {host}:{port}")
103
+
104
+ if args.reload:
105
+ print("Warning: --reload is enabled. This is not recommended for production.")
106
+ uvicorn.run(
107
+ "atlas.main:app",
108
+ host=host,
109
+ port=port,
110
+ reload=True,
111
+ workers=1, # reload doesn't support multiple workers
112
+ )
113
+ else:
114
+ uvicorn.run(
115
+ app,
116
+ host=host,
117
+ port=port,
118
+ workers=args.workers,
119
+ )
120
+
121
+ return 0
122
+
123
+
124
+ def main() -> None:
125
+ """Main entry point for atlas-server CLI."""
126
+ parser = build_parser()
127
+ args = parser.parse_args()
128
+
129
+ if args.version:
130
+ from atlas.version import VERSION
131
+ print(f"atlas-server version {VERSION}")
132
+ sys.exit(0)
133
+
134
+ # Apply env file first (before any other imports that might use env vars)
135
+ if args.env_file:
136
+ _apply_env_file(Path(args.env_file).expanduser())
137
+ else:
138
+ # Try to find .env in current directory or package root
139
+ cwd_env = Path.cwd() / ".env"
140
+ if cwd_env.exists():
141
+ _apply_env_file(cwd_env)
142
+ else:
143
+ # Check package root (parent of atlas/)
144
+ pkg_root_env = Path(__file__).resolve().parents[1] / ".env"
145
+ if pkg_root_env.exists():
146
+ _apply_env_file(pkg_root_env)
147
+
148
+ # Apply config folder if specified
149
+ if args.config_folder:
150
+ _apply_config_folder(Path(args.config_folder).expanduser())
151
+
152
+ # Apply config defaults if specified
153
+ if args.config_defaults:
154
+ defaults_path = Path(args.config_defaults).expanduser()
155
+ if not defaults_path.exists():
156
+ print(f"Error: config defaults folder not found: {defaults_path}", file=sys.stderr)
157
+ sys.exit(2)
158
+ os.environ["APP_CONFIG_DEFAULTS"] = str(defaults_path.resolve())
159
+
160
+ sys.exit(run_server(args))
161
+
162
+
163
+ if __name__ == "__main__":
164
+ main()
@@ -0,0 +1,20 @@
1
+ import sys
2
+ from pathlib import Path
3
+
4
+ # Ensure the backend root is on sys.path for absolute imports like 'infrastructure.*'
5
+ backend_root = Path(__file__).resolve().parents[1]
6
+ project_root = backend_root.parent
7
+ if str(backend_root) not in sys.path:
8
+ sys.path.insert(0, str(backend_root))
9
+ if str(project_root) not in sys.path:
10
+ sys.path.insert(0, str(project_root))
11
+
12
+ # Pre-import critical modules before any test files can replace them with fakes.
13
+ # This prevents test pollution where one test file patches sys.modules and other
14
+ # tests import the fake instead of the real module.
15
+ # See test_capability_tokens_and_injection.py which patches LiteLLMCaller.
16
+ import atlas.modules.llm.litellm_caller # noqa: E402, F401
17
+
18
+ # Explicitly reference the module to satisfy static analyzers that flag unused imports.
19
+ # The import above is intentional: it pre-populates sys.modules with the real module.
20
+ _ = atlas.modules.llm.litellm_caller.LiteLLMCaller # noqa: E402
@@ -0,0 +1,152 @@
1
+ from unittest.mock import ANY, AsyncMock, Mock, patch
2
+
3
+ import pytest
4
+
5
+ from atlas.modules.mcp_tools.client import MCPToolManager
6
+
7
+
8
+ @pytest.mark.integration
9
+ class TestMCPAuthenticationIntegration:
10
+ """Integration tests for MCP authentication."""
11
+
12
+ @pytest.mark.asyncio
13
+ async def test_authenticated_connection_success(self, monkeypatch):
14
+ """Should successfully connect to authenticated MCP server."""
15
+ monkeypatch.setenv("MCP_TEST_TOKEN", "test-api-key-123")
16
+
17
+ # Configure test server with auth requirement
18
+ server_config = {
19
+ "url": "http://localhost:8001/mcp",
20
+ "transport": "http",
21
+ "auth_token": "${MCP_TEST_TOKEN}"
22
+ }
23
+
24
+ # Mock config_manager to return our test server config
25
+ with patch('atlas.modules.mcp_tools.client.config_manager') as mock_config_manager:
26
+ mock_config_manager.mcp_config.servers = {"mcp-http-mock": Mock()}
27
+ mock_config_manager.mcp_config.servers["mcp-http-mock"].model_dump.return_value = server_config
28
+ mock_config_manager.app_settings.mcp_call_timeout = 120
29
+ mock_config_manager.app_settings.mcp_discovery_timeout = 30
30
+
31
+ manager = MCPToolManager()
32
+ manager.servers_config = {"mcp-http-mock": server_config}
33
+
34
+ # Mock the fastmcp.Client to avoid actual network call for now
35
+ with patch('atlas.modules.mcp_tools.client.Client') as MockFastMCPClient:
36
+ mock_client_instance = MockFastMCPClient.return_value
37
+ mock_client_instance.__aenter__.return_value = mock_client_instance
38
+ mock_client_instance.list_tools.return_value = [] # Mock an empty list of tools
39
+
40
+ await manager.initialize_clients()
41
+
42
+ # Assert that the client was initialized and added to manager.clients
43
+ assert "mcp-http-mock" in manager.clients
44
+ MockFastMCPClient.assert_called_once_with(
45
+ "http://localhost:8001/mcp",
46
+ auth="test-api-key-123",
47
+ log_handler=ANY,
48
+ elicitation_handler=ANY,
49
+ sampling_handler=ANY,
50
+ )
51
+
52
+ @pytest.mark.asyncio
53
+ async def test_authenticated_connection_failure_invalid_token(self, monkeypatch):
54
+ """Should fail to connect with invalid token."""
55
+ monkeypatch.setenv("MCP_TEST_TOKEN", "invalid-token") # Set an invalid token
56
+
57
+ server_config = {
58
+ "url": "http://localhost:8001/mcp",
59
+ "transport": "http",
60
+ "auth_token": "${MCP_TEST_TOKEN}"
61
+ }
62
+
63
+ with patch('atlas.modules.mcp_tools.client.config_manager') as mock_config_manager:
64
+ mock_config_manager.mcp_config.servers = {"mcp-http-mock": Mock()}
65
+ mock_config_manager.mcp_config.servers["mcp-http-mock"].model_dump.return_value = server_config
66
+ mock_config_manager.app_settings.mcp_call_timeout = 120
67
+ mock_config_manager.app_settings.mcp_discovery_timeout = 30
68
+
69
+ manager = MCPToolManager()
70
+ manager.servers_config = {"mcp-http-mock": server_config}
71
+
72
+ # Mock the fastmcp.Client to simulate an authentication error
73
+ with patch('atlas.modules.mcp_tools.client.Client') as MockFastMCPClient:
74
+ mock_client_instance = MockFastMCPClient.return_value
75
+ mock_client_instance.__aenter__.side_effect = Exception("Authentication failed: 401 Unauthorized")
76
+
77
+ await manager.initialize_clients()
78
+
79
+ # Client object is created successfully (not connected yet)
80
+ assert "mcp-http-mock" in manager.clients
81
+ # Verify auth token was passed correctly
82
+ MockFastMCPClient.assert_called_once_with(
83
+ "http://localhost:8001/mcp",
84
+ auth="invalid-token",
85
+ log_handler=ANY,
86
+ elicitation_handler=ANY,
87
+ sampling_handler=ANY,
88
+ )
89
+
90
+ # Now try to discover tools - this should fail due to auth error
91
+ await manager.discover_tools()
92
+
93
+ # After failed connection, tools should not be discovered
94
+ if not hasattr(manager, '_tool_index'):
95
+ manager._tool_index = {}
96
+ assert len([k for k in manager._tool_index.keys() if k.startswith("mcp-http-mock_")]) == 0
97
+
98
+ @pytest.mark.asyncio
99
+ async def test_authenticated_tool_execution(self, monkeypatch):
100
+ """Should successfully execute tool after authentication."""
101
+ monkeypatch.setenv("MCP_TEST_TOKEN", "test-api-key-123")
102
+
103
+ server_config = {
104
+ "url": "http://localhost:8001/mcp",
105
+ "transport": "http",
106
+ "auth_token": "${MCP_TEST_TOKEN}"
107
+ }
108
+
109
+ with patch('atlas.modules.mcp_tools.client.config_manager') as mock_config_manager:
110
+ mock_config_manager.mcp_config.servers = {"mcp-http-mock": Mock()}
111
+ mock_config_manager.mcp_config.servers["mcp-http-mock"].model_dump.return_value = server_config
112
+ mock_config_manager.app_settings.mcp_call_timeout = 120
113
+ mock_config_manager.app_settings.mcp_discovery_timeout = 30
114
+
115
+ manager = MCPToolManager()
116
+ manager.servers_config = {"mcp-http-mock": server_config}
117
+
118
+ # Mock the fastmcp.Client and its call_tool method
119
+ with patch('atlas.modules.mcp_tools.client.Client') as MockFastMCPClient:
120
+ mock_client_instance = MockFastMCPClient.return_value
121
+ mock_client_instance.__aenter__.return_value = mock_client_instance
122
+ mock_client_instance.list_tools.return_value = [] # Mock an empty list of tools
123
+
124
+ # Make call_tool return an async result
125
+ class MockResult:
126
+ data = {"results": "tool_result"}
127
+ mock_client_instance.call_tool = AsyncMock(return_value=MockResult())
128
+
129
+ await manager.initialize_clients()
130
+
131
+ # Simulate tool call
132
+ from atlas.domain.messages.models import ToolCall
133
+ tool_call = ToolCall(id="call_1", name="mcp-http-mock_test_tool", arguments={})
134
+
135
+ # Need to mock the _tool_index for execute_tool to find the server
136
+ mock_tool = Mock()
137
+ mock_tool.name = "test_tool"
138
+ manager._tool_index = {
139
+ "mcp-http-mock_test_tool": {
140
+ 'server': 'mcp-http-mock',
141
+ 'tool': mock_tool
142
+ }
143
+ }
144
+
145
+ result = await manager.execute_tool(tool_call)
146
+
147
+ assert result.success is True
148
+ assert "tool_result" in result.content
149
+ mock_client_instance.call_tool.assert_called_once()
150
+ call_args = mock_client_instance.call_tool.call_args
151
+ assert call_args[0] == ("test_tool", {})
152
+ assert "progress_handler" in call_args[1]
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Quick test script to verify sampling functionality works end-to-end.
4
+ This tests the sampling_demo server directly to ensure it can be initialized
5
+ and sampling works correctly.
6
+ """
7
+
8
+ import asyncio
9
+ import logging
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ # Add backend to path
14
+ sys.path.insert(0, str(Path(__file__).parent.parent))
15
+
16
+ logging.basicConfig(level=logging.INFO)
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ async def test_sampling_demo():
21
+ """Test the sampling_demo server."""
22
+ try:
23
+ from fastmcp import Client
24
+
25
+ logger.info("Starting sampling demo test...")
26
+
27
+ # Create sampling handler that uses LiteLLM
28
+ async def sampling_handler(messages, params, context):
29
+ from mcp.types import CreateMessageResult, TextContent
30
+
31
+ logger.info(f"Sampling handler called with {len(messages)} messages")
32
+
33
+ # For testing purposes, we'll return a mock response
34
+ # In production, this would call the actual LLM
35
+ logger.info("Returning mock LLM response for testing")
36
+
37
+ # Return proper CreateMessageResult
38
+ return CreateMessageResult(
39
+ role="assistant",
40
+ content=TextContent(type="text", text="This is a mock LLM response for testing purposes."),
41
+ model="mock-model"
42
+ )
43
+
44
+ # Create client to sampling_demo server
45
+ server_path = "backend/mcp/sampling_demo/main.py"
46
+ logger.info(f"Connecting to server: {server_path}")
47
+
48
+ client = Client(server_path, sampling_handler=sampling_handler)
49
+
50
+ async with client:
51
+ # List tools
52
+ tools = await client.list_tools()
53
+ logger.info(f"Available tools: {[t.name for t in tools]}")
54
+
55
+ # Test summarize_text tool
56
+ logger.info("\n=== Testing summarize_text ===")
57
+ result = await client.call_tool(
58
+ "summarize_text",
59
+ {
60
+ "text": "The quick brown fox jumps over the lazy dog. "
61
+ "This sentence contains every letter of the alphabet. "
62
+ "It is commonly used for testing fonts and keyboards."
63
+ }
64
+ )
65
+ logger.info(f"Result: {result}")
66
+
67
+ # Test analyze_sentiment tool
68
+ logger.info("\n=== Testing analyze_sentiment ===")
69
+ result = await client.call_tool(
70
+ "analyze_sentiment",
71
+ {
72
+ "text": "I absolutely love this product! It's amazing and works perfectly!"
73
+ }
74
+ )
75
+ logger.info(f"Result: {result}")
76
+
77
+ logger.info("\n✅ All tests passed!")
78
+ return True
79
+
80
+ except Exception as e:
81
+ logger.error(f"Test failed: {e}", exc_info=True)
82
+ return False
83
+
84
+
85
+ if __name__ == "__main__":
86
+ success = asyncio.run(test_sampling_demo())
87
+ sys.exit(0 if success else 1)