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.
- dtSpark/__init__.py +0 -0
- dtSpark/_description.txt +1 -0
- dtSpark/_full_name.txt +1 -0
- dtSpark/_licence.txt +21 -0
- dtSpark/_metadata.yaml +6 -0
- dtSpark/_name.txt +1 -0
- dtSpark/_version.txt +1 -0
- dtSpark/aws/__init__.py +7 -0
- dtSpark/aws/authentication.py +296 -0
- dtSpark/aws/bedrock.py +578 -0
- dtSpark/aws/costs.py +318 -0
- dtSpark/aws/pricing.py +580 -0
- dtSpark/cli_interface.py +2645 -0
- dtSpark/conversation_manager.py +3050 -0
- dtSpark/core/__init__.py +12 -0
- dtSpark/core/application.py +3355 -0
- dtSpark/core/context_compaction.py +735 -0
- dtSpark/daemon/__init__.py +104 -0
- dtSpark/daemon/__main__.py +10 -0
- dtSpark/daemon/action_monitor.py +213 -0
- dtSpark/daemon/daemon_app.py +730 -0
- dtSpark/daemon/daemon_manager.py +289 -0
- dtSpark/daemon/execution_coordinator.py +194 -0
- dtSpark/daemon/pid_file.py +169 -0
- dtSpark/database/__init__.py +482 -0
- dtSpark/database/autonomous_actions.py +1191 -0
- dtSpark/database/backends.py +329 -0
- dtSpark/database/connection.py +122 -0
- dtSpark/database/conversations.py +520 -0
- dtSpark/database/credential_prompt.py +218 -0
- dtSpark/database/files.py +205 -0
- dtSpark/database/mcp_ops.py +355 -0
- dtSpark/database/messages.py +161 -0
- dtSpark/database/schema.py +673 -0
- dtSpark/database/tool_permissions.py +186 -0
- dtSpark/database/usage.py +167 -0
- dtSpark/files/__init__.py +4 -0
- dtSpark/files/manager.py +322 -0
- dtSpark/launch.py +39 -0
- dtSpark/limits/__init__.py +10 -0
- dtSpark/limits/costs.py +296 -0
- dtSpark/limits/tokens.py +342 -0
- dtSpark/llm/__init__.py +17 -0
- dtSpark/llm/anthropic_direct.py +446 -0
- dtSpark/llm/base.py +146 -0
- dtSpark/llm/context_limits.py +438 -0
- dtSpark/llm/manager.py +177 -0
- dtSpark/llm/ollama.py +578 -0
- dtSpark/mcp_integration/__init__.py +5 -0
- dtSpark/mcp_integration/manager.py +653 -0
- dtSpark/mcp_integration/tool_selector.py +225 -0
- dtSpark/resources/config.yaml.template +631 -0
- dtSpark/safety/__init__.py +22 -0
- dtSpark/safety/llm_service.py +111 -0
- dtSpark/safety/patterns.py +229 -0
- dtSpark/safety/prompt_inspector.py +442 -0
- dtSpark/safety/violation_logger.py +346 -0
- dtSpark/scheduler/__init__.py +20 -0
- dtSpark/scheduler/creation_tools.py +599 -0
- dtSpark/scheduler/execution_queue.py +159 -0
- dtSpark/scheduler/executor.py +1152 -0
- dtSpark/scheduler/manager.py +395 -0
- dtSpark/tools/__init__.py +4 -0
- dtSpark/tools/builtin.py +833 -0
- dtSpark/web/__init__.py +20 -0
- dtSpark/web/auth.py +152 -0
- dtSpark/web/dependencies.py +37 -0
- dtSpark/web/endpoints/__init__.py +17 -0
- dtSpark/web/endpoints/autonomous_actions.py +1125 -0
- dtSpark/web/endpoints/chat.py +621 -0
- dtSpark/web/endpoints/conversations.py +353 -0
- dtSpark/web/endpoints/main_menu.py +547 -0
- dtSpark/web/endpoints/streaming.py +421 -0
- dtSpark/web/server.py +578 -0
- dtSpark/web/session.py +167 -0
- dtSpark/web/ssl_utils.py +195 -0
- dtSpark/web/static/css/dark-theme.css +427 -0
- dtSpark/web/static/js/actions.js +1101 -0
- dtSpark/web/static/js/chat.js +614 -0
- dtSpark/web/static/js/main.js +496 -0
- dtSpark/web/static/js/sse-client.js +242 -0
- dtSpark/web/templates/actions.html +408 -0
- dtSpark/web/templates/base.html +93 -0
- dtSpark/web/templates/chat.html +814 -0
- dtSpark/web/templates/conversations.html +350 -0
- dtSpark/web/templates/goodbye.html +81 -0
- dtSpark/web/templates/login.html +90 -0
- dtSpark/web/templates/main_menu.html +983 -0
- dtSpark/web/templates/new_conversation.html +191 -0
- dtSpark/web/web_interface.py +137 -0
- dtspark-1.0.4.dist-info/METADATA +187 -0
- dtspark-1.0.4.dist-info/RECORD +96 -0
- dtspark-1.0.4.dist-info/WHEEL +5 -0
- dtspark-1.0.4.dist-info/entry_points.txt +3 -0
- dtspark-1.0.4.dist-info/licenses/LICENSE +21 -0
- 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
|