dtSpark 1.0.4__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 (96) hide show
  1. dtSpark/__init__.py +0 -0
  2. dtSpark/_description.txt +1 -0
  3. dtSpark/_full_name.txt +1 -0
  4. dtSpark/_licence.txt +21 -0
  5. dtSpark/_metadata.yaml +6 -0
  6. dtSpark/_name.txt +1 -0
  7. dtSpark/_version.txt +1 -0
  8. dtSpark/aws/__init__.py +7 -0
  9. dtSpark/aws/authentication.py +296 -0
  10. dtSpark/aws/bedrock.py +578 -0
  11. dtSpark/aws/costs.py +318 -0
  12. dtSpark/aws/pricing.py +580 -0
  13. dtSpark/cli_interface.py +2645 -0
  14. dtSpark/conversation_manager.py +3050 -0
  15. dtSpark/core/__init__.py +12 -0
  16. dtSpark/core/application.py +3355 -0
  17. dtSpark/core/context_compaction.py +735 -0
  18. dtSpark/daemon/__init__.py +104 -0
  19. dtSpark/daemon/__main__.py +10 -0
  20. dtSpark/daemon/action_monitor.py +213 -0
  21. dtSpark/daemon/daemon_app.py +730 -0
  22. dtSpark/daemon/daemon_manager.py +289 -0
  23. dtSpark/daemon/execution_coordinator.py +194 -0
  24. dtSpark/daemon/pid_file.py +169 -0
  25. dtSpark/database/__init__.py +482 -0
  26. dtSpark/database/autonomous_actions.py +1191 -0
  27. dtSpark/database/backends.py +329 -0
  28. dtSpark/database/connection.py +122 -0
  29. dtSpark/database/conversations.py +520 -0
  30. dtSpark/database/credential_prompt.py +218 -0
  31. dtSpark/database/files.py +205 -0
  32. dtSpark/database/mcp_ops.py +355 -0
  33. dtSpark/database/messages.py +161 -0
  34. dtSpark/database/schema.py +673 -0
  35. dtSpark/database/tool_permissions.py +186 -0
  36. dtSpark/database/usage.py +167 -0
  37. dtSpark/files/__init__.py +4 -0
  38. dtSpark/files/manager.py +322 -0
  39. dtSpark/launch.py +39 -0
  40. dtSpark/limits/__init__.py +10 -0
  41. dtSpark/limits/costs.py +296 -0
  42. dtSpark/limits/tokens.py +342 -0
  43. dtSpark/llm/__init__.py +17 -0
  44. dtSpark/llm/anthropic_direct.py +446 -0
  45. dtSpark/llm/base.py +146 -0
  46. dtSpark/llm/context_limits.py +438 -0
  47. dtSpark/llm/manager.py +177 -0
  48. dtSpark/llm/ollama.py +578 -0
  49. dtSpark/mcp_integration/__init__.py +5 -0
  50. dtSpark/mcp_integration/manager.py +653 -0
  51. dtSpark/mcp_integration/tool_selector.py +225 -0
  52. dtSpark/resources/config.yaml.template +631 -0
  53. dtSpark/safety/__init__.py +22 -0
  54. dtSpark/safety/llm_service.py +111 -0
  55. dtSpark/safety/patterns.py +229 -0
  56. dtSpark/safety/prompt_inspector.py +442 -0
  57. dtSpark/safety/violation_logger.py +346 -0
  58. dtSpark/scheduler/__init__.py +20 -0
  59. dtSpark/scheduler/creation_tools.py +599 -0
  60. dtSpark/scheduler/execution_queue.py +159 -0
  61. dtSpark/scheduler/executor.py +1152 -0
  62. dtSpark/scheduler/manager.py +395 -0
  63. dtSpark/tools/__init__.py +4 -0
  64. dtSpark/tools/builtin.py +833 -0
  65. dtSpark/web/__init__.py +20 -0
  66. dtSpark/web/auth.py +152 -0
  67. dtSpark/web/dependencies.py +37 -0
  68. dtSpark/web/endpoints/__init__.py +17 -0
  69. dtSpark/web/endpoints/autonomous_actions.py +1125 -0
  70. dtSpark/web/endpoints/chat.py +621 -0
  71. dtSpark/web/endpoints/conversations.py +353 -0
  72. dtSpark/web/endpoints/main_menu.py +547 -0
  73. dtSpark/web/endpoints/streaming.py +421 -0
  74. dtSpark/web/server.py +578 -0
  75. dtSpark/web/session.py +167 -0
  76. dtSpark/web/ssl_utils.py +195 -0
  77. dtSpark/web/static/css/dark-theme.css +427 -0
  78. dtSpark/web/static/js/actions.js +1101 -0
  79. dtSpark/web/static/js/chat.js +614 -0
  80. dtSpark/web/static/js/main.js +496 -0
  81. dtSpark/web/static/js/sse-client.js +242 -0
  82. dtSpark/web/templates/actions.html +408 -0
  83. dtSpark/web/templates/base.html +93 -0
  84. dtSpark/web/templates/chat.html +814 -0
  85. dtSpark/web/templates/conversations.html +350 -0
  86. dtSpark/web/templates/goodbye.html +81 -0
  87. dtSpark/web/templates/login.html +90 -0
  88. dtSpark/web/templates/main_menu.html +983 -0
  89. dtSpark/web/templates/new_conversation.html +191 -0
  90. dtSpark/web/web_interface.py +137 -0
  91. dtspark-1.0.4.dist-info/METADATA +187 -0
  92. dtspark-1.0.4.dist-info/RECORD +96 -0
  93. dtspark-1.0.4.dist-info/WHEEL +5 -0
  94. dtspark-1.0.4.dist-info/entry_points.txt +3 -0
  95. dtspark-1.0.4.dist-info/licenses/LICENSE +21 -0
  96. dtspark-1.0.4.dist-info/top_level.txt +1 -0
@@ -0,0 +1,653 @@
1
+ """
2
+ MCP Manager module for managing Model Context Protocol server connections.
3
+
4
+ This module provides functionality for:
5
+ - Connecting to MCP servers via stdio, HTTP, or SSE transports
6
+ - Authentication support (Bearer tokens, API keys, Basic auth, custom headers)
7
+ - Listing available tools from connected servers
8
+ - Calling tools on MCP servers
9
+ - Managing server lifecycles
10
+ """
11
+
12
+ import asyncio
13
+ import logging
14
+ import base64
15
+ from typing import Dict, List, Optional, Any
16
+ from dataclasses import dataclass, field
17
+
18
+ try:
19
+ from mcp import ClientSession, StdioServerParameters
20
+ from mcp.client.stdio import stdio_client
21
+ from mcp.client.streamable_http import streamablehttp_client
22
+ from mcp.client.sse import sse_client
23
+ import mcp.types as types
24
+ from pydantic import AnyUrl
25
+ import httpx
26
+ MCP_AVAILABLE = True
27
+ except ImportError as e:
28
+ MCP_AVAILABLE = False
29
+ logging.warning(f"MCP library not available. Import error: {e}")
30
+ logging.warning("Install required packages with: pip install 'mcp>=1.1.0' 'pydantic>=2.0.0' 'httpx'")
31
+
32
+
33
+ @dataclass
34
+ class MCPServerConfig:
35
+ """
36
+ Configuration for an MCP server.
37
+
38
+ Attributes:
39
+ name: Unique identifier for this server
40
+ transport: Transport type - 'stdio', 'http', or 'sse'
41
+ command: Command to run for stdio transport
42
+ args: Arguments for stdio command
43
+ url: URL for http/sse transports
44
+ env: Environment variables for stdio transport
45
+ enabled: Whether this server is enabled
46
+ auth_type: Authentication type - 'none', 'bearer', 'api_key', 'basic', 'custom'
47
+ auth_token: Bearer token or API key value
48
+ auth_header_name: Custom header name for api_key auth (default: 'X-API-Key')
49
+ basic_username: Username for basic auth
50
+ basic_password: Password for basic auth
51
+ custom_headers: Additional custom headers as dict
52
+ timeout: Connection timeout in seconds
53
+ ssl_verify: Whether to verify SSL certificates (default: True)
54
+ Set to False for self-signed certificates
55
+ """
56
+ name: str
57
+ transport: str # 'stdio', 'http', or 'sse'
58
+ command: Optional[str] = None
59
+ args: Optional[List[str]] = None
60
+ url: Optional[str] = None
61
+ env: Optional[Dict[str, str]] = None
62
+ enabled: bool = True
63
+ # Authentication options
64
+ auth_type: str = 'none' # 'none', 'bearer', 'api_key', 'basic', 'custom'
65
+ auth_token: Optional[str] = None
66
+ auth_header_name: str = 'X-API-Key' # For api_key auth type
67
+ basic_username: Optional[str] = None
68
+ basic_password: Optional[str] = None
69
+ custom_headers: Optional[Dict[str, str]] = None
70
+ timeout: float = 30.0
71
+ # SSL options
72
+ ssl_verify: bool = True # Set to False for self-signed certificates
73
+
74
+
75
+ class MCPClient:
76
+ """Manages a single MCP server connection."""
77
+
78
+ def __init__(self, config: MCPServerConfig):
79
+ """
80
+ Initialise an MCP client for a specific server.
81
+
82
+ Args:
83
+ config: Server configuration
84
+ """
85
+ self.config = config
86
+ self.session: Optional[ClientSession] = None
87
+ self.read = None
88
+ self.write = None
89
+ self._context = None
90
+ self._connected = False
91
+ self._httpx_client = None # Custom httpx client for SSL options
92
+
93
+ def _build_auth_headers(self) -> Dict[str, str]:
94
+ """
95
+ Build authentication headers based on configuration.
96
+
97
+ Returns:
98
+ Dictionary of headers for authentication
99
+ """
100
+ headers = {}
101
+
102
+ # Add custom headers first (can be overridden by auth)
103
+ if self.config.custom_headers:
104
+ headers.update(self.config.custom_headers)
105
+
106
+ auth_type = self.config.auth_type.lower()
107
+
108
+ if auth_type == 'bearer':
109
+ if self.config.auth_token:
110
+ headers['Authorization'] = f'Bearer {self.config.auth_token}'
111
+ else:
112
+ logging.warning(f"Bearer auth selected but no auth_token provided for {self.config.name}")
113
+
114
+ elif auth_type == 'api_key':
115
+ if self.config.auth_token:
116
+ header_name = self.config.auth_header_name or 'X-API-Key'
117
+ headers[header_name] = self.config.auth_token
118
+ else:
119
+ logging.warning(f"API key auth selected but no auth_token provided for {self.config.name}")
120
+
121
+ elif auth_type == 'basic':
122
+ if self.config.basic_username and self.config.basic_password:
123
+ credentials = f"{self.config.basic_username}:{self.config.basic_password}"
124
+ encoded = base64.b64encode(credentials.encode()).decode()
125
+ headers['Authorization'] = f'Basic {encoded}'
126
+ else:
127
+ logging.warning(f"Basic auth selected but credentials incomplete for {self.config.name}")
128
+
129
+ elif auth_type == 'custom':
130
+ # Custom auth relies entirely on custom_headers
131
+ if not self.config.custom_headers:
132
+ logging.warning(f"Custom auth selected but no custom_headers provided for {self.config.name}")
133
+
134
+ elif auth_type != 'none':
135
+ logging.warning(f"Unknown auth_type '{auth_type}' for {self.config.name}")
136
+
137
+ return headers
138
+
139
+ async def connect(self) -> bool:
140
+ """
141
+ Connect to the MCP server.
142
+
143
+ Returns:
144
+ True if connection successful, False otherwise
145
+ """
146
+ if not MCP_AVAILABLE:
147
+ logging.error(f"Cannot connect to {self.config.name}: MCP library not installed")
148
+ return False
149
+
150
+ try:
151
+ if self.config.transport == 'stdio':
152
+ if not self.config.command:
153
+ logging.error(f"stdio transport requires a command for {self.config.name}")
154
+ return False
155
+
156
+ server_params = StdioServerParameters(
157
+ command=self.config.command,
158
+ args=self.config.args or [],
159
+ env=self.config.env or None
160
+ )
161
+
162
+ self._context = stdio_client(server_params)
163
+ self.read, self.write = await self._context.__aenter__()
164
+
165
+ elif self.config.transport == 'http':
166
+ if not self.config.url:
167
+ logging.error(f"HTTP transport requires a URL for {self.config.name}")
168
+ return False
169
+
170
+ # Build authentication headers
171
+ headers = self._build_auth_headers()
172
+
173
+ logging.debug(f"Connecting to HTTP MCP server {self.config.name} at {self.config.url}")
174
+ if headers:
175
+ logging.debug(f"Using {len(headers)} custom headers for authentication")
176
+
177
+ # Log SSL verification status
178
+ if not self.config.ssl_verify:
179
+ logging.warning(f"SSL certificate verification disabled for {self.config.name} "
180
+ f"(not recommended for production)")
181
+
182
+ # Create custom httpx client if SSL verification is disabled
183
+ httpx_client = None
184
+ if not self.config.ssl_verify:
185
+ httpx_client = httpx.AsyncClient(
186
+ headers=headers if headers else None,
187
+ timeout=self.config.timeout,
188
+ verify=False
189
+ )
190
+
191
+ # Use streamable HTTP client with headers
192
+ if httpx_client:
193
+ self._httpx_client = httpx_client # Store for cleanup
194
+ self._context = streamablehttp_client(
195
+ url=self.config.url,
196
+ httpx_client=httpx_client
197
+ )
198
+ else:
199
+ self._context = streamablehttp_client(
200
+ url=self.config.url,
201
+ headers=headers if headers else None,
202
+ timeout=self.config.timeout
203
+ )
204
+ self.read, self.write, _ = await self._context.__aenter__()
205
+
206
+ elif self.config.transport == 'sse':
207
+ if not self.config.url:
208
+ logging.error(f"SSE transport requires a URL for {self.config.name}")
209
+ return False
210
+
211
+ # Build authentication headers
212
+ headers = self._build_auth_headers()
213
+
214
+ logging.debug(f"Connecting to SSE MCP server {self.config.name} at {self.config.url}")
215
+ if headers:
216
+ logging.debug(f"Using {len(headers)} custom headers for authentication")
217
+
218
+ # Log SSL verification status
219
+ if not self.config.ssl_verify:
220
+ logging.warning(f"SSL certificate verification disabled for {self.config.name} "
221
+ f"(not recommended for production)")
222
+
223
+ # Create custom httpx client if SSL verification is disabled
224
+ httpx_client = None
225
+ if not self.config.ssl_verify:
226
+ httpx_client = httpx.AsyncClient(
227
+ headers=headers if headers else None,
228
+ timeout=self.config.timeout,
229
+ verify=False
230
+ )
231
+
232
+ # Use SSE client with headers
233
+ if httpx_client:
234
+ self._httpx_client = httpx_client # Store for cleanup
235
+ self._context = sse_client(
236
+ url=self.config.url,
237
+ httpx_client=httpx_client
238
+ )
239
+ else:
240
+ self._context = sse_client(
241
+ url=self.config.url,
242
+ headers=headers if headers else None,
243
+ timeout=self.config.timeout
244
+ )
245
+ self.read, self.write = await self._context.__aenter__()
246
+
247
+ else:
248
+ logging.error(f"Unknown transport type: {self.config.transport}")
249
+ return False
250
+
251
+ # Create session and initialize with timeout
252
+ self.session = ClientSession(self.read, self.write)
253
+ await self.session.__aenter__()
254
+
255
+ # Apply timeout to initialization to prevent hanging on failed connections
256
+ try:
257
+ await asyncio.wait_for(
258
+ self.session.initialize(),
259
+ timeout=self.config.timeout
260
+ )
261
+ except asyncio.TimeoutError:
262
+ logging.error(f"Timeout during MCP session initialization for {self.config.name}")
263
+ await self._cleanup_failed_connection()
264
+ return False
265
+ except asyncio.CancelledError:
266
+ logging.error(f"MCP session initialization cancelled for {self.config.name} "
267
+ f"(server may have returned an error)")
268
+ await self._cleanup_failed_connection()
269
+ return False
270
+
271
+ self._connected = True
272
+ logging.info(f"Connected to MCP server: {self.config.name} (transport: {self.config.transport})")
273
+ return True
274
+
275
+ except asyncio.CancelledError:
276
+ logging.error(f"Connection cancelled for MCP server {self.config.name} "
277
+ f"(check server URL and authentication)")
278
+ await self._cleanup_failed_connection()
279
+ return False
280
+ except Exception as e:
281
+ error_msg = str(e)
282
+ # Provide more helpful error messages for common issues
283
+ if '401' in error_msg or 'Unauthorized' in error_msg:
284
+ logging.error(f"Authentication failed for MCP server {self.config.name}: "
285
+ f"Check auth_type and credentials")
286
+ elif '403' in error_msg or 'Forbidden' in error_msg:
287
+ logging.error(f"Access forbidden for MCP server {self.config.name}: "
288
+ f"Check permissions and API key")
289
+ elif '404' in error_msg or 'Not Found' in error_msg:
290
+ logging.error(f"MCP endpoint not found for {self.config.name}: "
291
+ f"Check URL is correct ({self.config.url})")
292
+ elif 'Connection refused' in error_msg:
293
+ logging.error(f"Connection refused for MCP server {self.config.name}: "
294
+ f"Check server is running at {self.config.url}")
295
+ else:
296
+ logging.error(f"Failed to connect to MCP server {self.config.name}: {e}")
297
+ await self._cleanup_failed_connection()
298
+ return False
299
+
300
+ async def _cleanup_failed_connection(self):
301
+ """Clean up resources after a failed connection attempt."""
302
+ try:
303
+ if self.session:
304
+ try:
305
+ await self.session.__aexit__(None, None, None)
306
+ except Exception:
307
+ pass
308
+ self.session = None
309
+
310
+ if self._context:
311
+ try:
312
+ await self._context.__aexit__(None, None, None)
313
+ except Exception:
314
+ pass
315
+ self._context = None
316
+
317
+ if self._httpx_client:
318
+ try:
319
+ await self._httpx_client.aclose()
320
+ except Exception:
321
+ pass
322
+ self._httpx_client = None
323
+
324
+ self.read = None
325
+ self.write = None
326
+ self._connected = False
327
+ except Exception as e:
328
+ logging.debug(f"Error during connection cleanup for {self.config.name}: {e}")
329
+
330
+ async def disconnect(self):
331
+ """Disconnect from the MCP server."""
332
+ try:
333
+ if self.session:
334
+ await self.session.__aexit__(None, None, None)
335
+ self.session = None
336
+
337
+ if self._context:
338
+ await self._context.__aexit__(None, None, None)
339
+ self._context = None
340
+
341
+ if self._httpx_client:
342
+ await self._httpx_client.aclose()
343
+ self._httpx_client = None
344
+
345
+ self._connected = False
346
+ logging.info(f"Disconnected from MCP server: {self.config.name}")
347
+
348
+ except Exception as e:
349
+ logging.error(f"Error disconnecting from {self.config.name}: {e}")
350
+
351
+ @property
352
+ def connected(self) -> bool:
353
+ """Check if the client is connected."""
354
+ return self._connected
355
+
356
+ async def list_tools(self) -> List[Dict[str, Any]]:
357
+ """
358
+ List all available tools from this server.
359
+
360
+ Returns:
361
+ List of tool dictionaries with name, description, and schema
362
+ """
363
+ if not self.session or not self._connected:
364
+ logging.warning(f"Cannot list tools: not connected to {self.config.name}")
365
+ return []
366
+
367
+ try:
368
+ result = await self.session.list_tools()
369
+ tools = []
370
+
371
+ for tool in result.tools:
372
+ tools.append({
373
+ 'name': tool.name,
374
+ 'description': tool.description or '',
375
+ 'input_schema': tool.inputSchema,
376
+ 'server': self.config.name
377
+ })
378
+
379
+ logging.debug(f"Found {len(tools)} tools from {self.config.name}")
380
+ return tools
381
+
382
+ except Exception as e:
383
+ logging.error(f"Failed to list tools from {self.config.name}: {e}")
384
+ return []
385
+
386
+ async def call_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Optional[Dict[str, Any]]:
387
+ """
388
+ Call a tool on this server.
389
+
390
+ Args:
391
+ tool_name: Name of the tool to call
392
+ arguments: Tool arguments
393
+
394
+ Returns:
395
+ Tool result dictionary or None on failure
396
+ """
397
+ if not self.session or not self._connected:
398
+ logging.warning(f"Cannot call tool: not connected to {self.config.name}")
399
+ return None
400
+
401
+ try:
402
+ logging.debug(f"Calling tool {tool_name} on {self.config.name} with args: {arguments}")
403
+ result = await self.session.call_tool(tool_name, arguments)
404
+
405
+ # Parse the result
406
+ content = []
407
+ for item in result.content:
408
+ if isinstance(item, types.TextContent):
409
+ content.append({
410
+ 'type': 'text',
411
+ 'text': item.text
412
+ })
413
+ elif isinstance(item, types.ImageContent):
414
+ content.append({
415
+ 'type': 'image',
416
+ 'data': item.data,
417
+ 'mimeType': item.mimeType
418
+ })
419
+ elif isinstance(item, types.EmbeddedResource):
420
+ content.append({
421
+ 'type': 'resource',
422
+ 'resource': item.resource
423
+ })
424
+
425
+ return {
426
+ 'content': content,
427
+ 'isError': result.isError if hasattr(result, 'isError') else False
428
+ }
429
+
430
+ except Exception as e:
431
+ logging.error(f"Failed to call tool {tool_name} on {self.config.name}: {e}")
432
+ return {
433
+ 'content': [{'type': 'text', 'text': f"Error calling tool: {str(e)}"}],
434
+ 'isError': True
435
+ }
436
+
437
+
438
+ class MCPManager:
439
+ """Manages multiple MCP server connections."""
440
+
441
+ def __init__(self):
442
+ """Initialise the MCP manager."""
443
+ self.clients: Dict[str, MCPClient] = {}
444
+ self._loop = None
445
+ self._tools_cache: Optional[List[Dict[str, Any]]] = None
446
+ self._initialization_loop = None # Store the loop used during init
447
+
448
+ def add_server(self, config: MCPServerConfig):
449
+ """
450
+ Add an MCP server configuration.
451
+
452
+ Args:
453
+ config: Server configuration
454
+ """
455
+ if not config.enabled:
456
+ logging.info(f"Skipping disabled MCP server: {config.name}")
457
+ return
458
+
459
+ if config.name in self.clients:
460
+ logging.warning(f"MCP server {config.name} already exists, replacing")
461
+
462
+ self.clients[config.name] = MCPClient(config)
463
+ logging.info(f"Added MCP server configuration: {config.name}")
464
+
465
+ async def connect_all(self, progress_callback=None) -> Dict[str, bool]:
466
+ """
467
+ Connect to all configured MCP servers.
468
+
469
+ Args:
470
+ progress_callback: Optional callback function(server_name, success) called after each server connection
471
+
472
+ Returns:
473
+ Dictionary mapping server names to connection status
474
+ """
475
+ if not self.clients:
476
+ logging.info("No MCP servers configured")
477
+ return {}
478
+
479
+ results = {}
480
+ for name, client in self.clients.items():
481
+ success = await client.connect()
482
+ results[name] = success
483
+
484
+ # Call progress callback after each server connection
485
+ if progress_callback:
486
+ progress_callback(name, success)
487
+
488
+ connected_count = sum(1 for success in results.values() if success)
489
+ logging.info(f"Connected to {connected_count}/{len(self.clients)} MCP servers")
490
+
491
+ return results
492
+
493
+ async def disconnect_all(self):
494
+ """Disconnect from all MCP servers."""
495
+ for client in self.clients.values():
496
+ await client.disconnect()
497
+
498
+ async def list_all_tools(self) -> List[Dict[str, Any]]:
499
+ """
500
+ List all available tools from all connected servers.
501
+ Results are cached after first successful fetch.
502
+
503
+ Returns:
504
+ List of tool dictionaries
505
+ """
506
+ # Return cached tools if available
507
+ if self._tools_cache is not None:
508
+ logging.debug(f"Returning cached tools: {len(self._tools_cache)}")
509
+ return self._tools_cache
510
+
511
+ all_tools = []
512
+
513
+ for client in self.clients.values():
514
+ if client.connected:
515
+ try:
516
+ # Add timeout to each client's list_tools call
517
+ tools = await asyncio.wait_for(client.list_tools(), timeout=5.0)
518
+ all_tools.extend(tools)
519
+ except asyncio.TimeoutError:
520
+ logging.error(f"Timeout listing tools from {client.config.name}")
521
+ except Exception as e:
522
+ logging.error(f"Error listing tools from {client.config.name}: {e}")
523
+
524
+ logging.info(f"Found {len(all_tools)} total tools across all MCP servers")
525
+
526
+ # Cache the results
527
+ self._tools_cache = all_tools
528
+
529
+ return all_tools
530
+
531
+ async def call_tool(self, tool_name: str, arguments: Dict[str, Any],
532
+ server_name: Optional[str] = None) -> Optional[Dict[str, Any]]:
533
+ """
534
+ Call a tool on an MCP server.
535
+
536
+ Args:
537
+ tool_name: Name of the tool to call
538
+ arguments: Tool arguments
539
+ server_name: Optional server name (if None, searches all servers)
540
+
541
+ Returns:
542
+ Tool result or None on failure
543
+ """
544
+ # If server is specified, call on that server
545
+ if server_name:
546
+ client = self.clients.get(server_name)
547
+ if not client:
548
+ logging.error(f"Server {server_name} not found")
549
+ return None
550
+ return await client.call_tool(tool_name, arguments)
551
+
552
+ # Otherwise, search for the tool across all servers
553
+ for client in self.clients.values():
554
+ if client.connected:
555
+ tools = await client.list_tools()
556
+ if any(tool['name'] == tool_name for tool in tools):
557
+ return await client.call_tool(tool_name, arguments)
558
+
559
+ logging.error(f"Tool {tool_name} not found on any connected server")
560
+ return None
561
+
562
+ def get_tools_schema(self) -> List[Dict[str, Any]]:
563
+ """
564
+ Get tools schema in Claude-compatible format.
565
+
566
+ Returns:
567
+ List of tool definitions for Claude API
568
+ """
569
+ # This needs to be called from an async context
570
+ # We'll need to handle this in the conversation manager
571
+ return []
572
+
573
+ @classmethod
574
+ def from_config(cls, config_dict: Dict[str, Any]) -> 'MCPManager':
575
+ """
576
+ Create an MCP manager from configuration dictionary.
577
+
578
+ Args:
579
+ config_dict: Configuration dictionary with MCP server definitions
580
+
581
+ Returns:
582
+ Configured MCPManager instance
583
+
584
+ Example configuration:
585
+ mcp_config:
586
+ servers:
587
+ # Local stdio server
588
+ - name: local-tools
589
+ transport: stdio
590
+ command: python
591
+ args: ["-m", "my_mcp_server"]
592
+ enabled: true
593
+
594
+ # Remote HTTP server with bearer auth
595
+ - name: remote-api
596
+ transport: http
597
+ url: https://api.example.com/mcp
598
+ auth_type: bearer
599
+ auth_token: ${REMOTE_API_TOKEN}
600
+ timeout: 60
601
+
602
+ # SSE server with API key auth
603
+ - name: sse-service
604
+ transport: sse
605
+ url: https://events.example.com/mcp
606
+ auth_type: api_key
607
+ auth_token: ${SSE_API_KEY}
608
+ auth_header_name: X-API-Key
609
+
610
+ # HTTP server with basic auth
611
+ - name: internal-service
612
+ transport: http
613
+ url: https://internal.example.com/mcp
614
+ auth_type: basic
615
+ basic_username: ${SERVICE_USER}
616
+ basic_password: ${SERVICE_PASS}
617
+
618
+ # HTTP server with custom headers
619
+ - name: custom-auth-service
620
+ transport: http
621
+ url: https://custom.example.com/mcp
622
+ auth_type: custom
623
+ custom_headers:
624
+ X-Tenant-ID: "my-tenant"
625
+ X-Custom-Auth: "secret-value"
626
+ """
627
+ manager = cls()
628
+
629
+ servers_config = config_dict.get('mcp_config', {}).get('servers', [])
630
+
631
+ for server_config in servers_config:
632
+ config = MCPServerConfig(
633
+ name=server_config.get('name', 'unknown'),
634
+ transport=server_config.get('transport', 'stdio'),
635
+ command=server_config.get('command'),
636
+ args=server_config.get('args', []),
637
+ url=server_config.get('url'),
638
+ env=server_config.get('env'),
639
+ enabled=server_config.get('enabled', True),
640
+ # Authentication options
641
+ auth_type=server_config.get('auth_type', 'none'),
642
+ auth_token=server_config.get('auth_token'),
643
+ auth_header_name=server_config.get('auth_header_name', 'X-API-Key'),
644
+ basic_username=server_config.get('basic_username'),
645
+ basic_password=server_config.get('basic_password'),
646
+ custom_headers=server_config.get('custom_headers'),
647
+ timeout=server_config.get('timeout', 30.0),
648
+ # SSL options
649
+ ssl_verify=server_config.get('ssl_verify', True)
650
+ )
651
+ manager.add_server(config)
652
+
653
+ return manager