mseep-lightfast-mcp 0.0.1__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 (43) hide show
  1. common/__init__.py +21 -0
  2. common/types.py +182 -0
  3. lightfast_mcp/__init__.py +50 -0
  4. lightfast_mcp/core/__init__.py +14 -0
  5. lightfast_mcp/core/base_server.py +205 -0
  6. lightfast_mcp/exceptions.py +55 -0
  7. lightfast_mcp/servers/__init__.py +1 -0
  8. lightfast_mcp/servers/blender/__init__.py +5 -0
  9. lightfast_mcp/servers/blender/server.py +358 -0
  10. lightfast_mcp/servers/blender_mcp_server.py +82 -0
  11. lightfast_mcp/servers/mock/__init__.py +5 -0
  12. lightfast_mcp/servers/mock/server.py +101 -0
  13. lightfast_mcp/servers/mock/tools.py +161 -0
  14. lightfast_mcp/servers/mock_server.py +78 -0
  15. lightfast_mcp/utils/__init__.py +1 -0
  16. lightfast_mcp/utils/logging_utils.py +69 -0
  17. mseep_lightfast_mcp-0.0.1.dist-info/METADATA +36 -0
  18. mseep_lightfast_mcp-0.0.1.dist-info/RECORD +43 -0
  19. mseep_lightfast_mcp-0.0.1.dist-info/WHEEL +5 -0
  20. mseep_lightfast_mcp-0.0.1.dist-info/entry_points.txt +7 -0
  21. mseep_lightfast_mcp-0.0.1.dist-info/licenses/LICENSE +21 -0
  22. mseep_lightfast_mcp-0.0.1.dist-info/top_level.txt +3 -0
  23. tools/__init__.py +46 -0
  24. tools/ai/__init__.py +8 -0
  25. tools/ai/conversation_cli.py +345 -0
  26. tools/ai/conversation_client.py +399 -0
  27. tools/ai/conversation_session.py +342 -0
  28. tools/ai/providers/__init__.py +11 -0
  29. tools/ai/providers/base_provider.py +64 -0
  30. tools/ai/providers/claude_provider.py +200 -0
  31. tools/ai/providers/openai_provider.py +204 -0
  32. tools/ai/tool_executor.py +257 -0
  33. tools/common/__init__.py +99 -0
  34. tools/common/async_utils.py +419 -0
  35. tools/common/errors.py +222 -0
  36. tools/common/logging.py +252 -0
  37. tools/common/types.py +130 -0
  38. tools/orchestration/__init__.py +15 -0
  39. tools/orchestration/cli.py +320 -0
  40. tools/orchestration/config_loader.py +348 -0
  41. tools/orchestration/server_orchestrator.py +466 -0
  42. tools/orchestration/server_registry.py +187 -0
  43. tools/orchestration/server_selector.py +242 -0
@@ -0,0 +1,399 @@
1
+ """Conversation client for managing AI conversations across multiple MCP servers."""
2
+
3
+ import os
4
+ import uuid
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ import mcp.types as mcp_types
8
+
9
+ from tools.common import (
10
+ AIProviderError,
11
+ ConversationResult,
12
+ ConversationStep,
13
+ OperationStatus,
14
+ Result,
15
+ ToolCall,
16
+ ToolResult,
17
+ get_connection_pool,
18
+ get_logger,
19
+ with_correlation_id,
20
+ with_operation_context,
21
+ )
22
+ from tools.common.async_utils import ConnectionPool
23
+
24
+ from .conversation_session import ConversationSession
25
+ from .providers.base_provider import BaseAIProvider
26
+ from .providers.claude_provider import ClaudeProvider
27
+ from .providers.openai_provider import OpenAIProvider
28
+ from .tool_executor import ToolExecutor
29
+
30
+ logger = get_logger("ConversationClient")
31
+
32
+
33
+ class ConversationClient:
34
+ """Manages AI conversations across multiple MCP servers."""
35
+
36
+ def __init__(
37
+ self,
38
+ servers: Dict[str, Dict[str, Any]],
39
+ ai_provider: str = "claude",
40
+ api_key: Optional[str] = None,
41
+ max_steps: int = 5,
42
+ max_concurrent_tools: int = 5,
43
+ ):
44
+ """Initialize the conversation client."""
45
+ self.servers = servers
46
+ self.ai_provider_name = ai_provider.lower()
47
+ self.api_key = api_key or self._get_api_key()
48
+ self.max_steps = max_steps
49
+ self.max_concurrent_tools = max_concurrent_tools
50
+
51
+ # Initialize components
52
+ self.ai_provider = self._create_ai_provider()
53
+ self.tool_executor = ToolExecutor(max_concurrent=max_concurrent_tools)
54
+ self.connection_pool: Optional[ConnectionPool] = None
55
+
56
+ # Server and tool tracking
57
+ self.connected_servers: Dict[str, Dict[str, Any]] = {}
58
+ self.available_tools: Dict[str, tuple[mcp_types.Tool, str]] = {}
59
+
60
+ # Active sessions
61
+ self.active_sessions: Dict[str, ConversationSession] = {}
62
+
63
+ def _get_api_key(self) -> str:
64
+ """Get API key from environment variables."""
65
+ if self.ai_provider_name == "claude":
66
+ key = os.getenv("ANTHROPIC_API_KEY")
67
+ if not key:
68
+ raise AIProviderError(
69
+ "ANTHROPIC_API_KEY environment variable required for Claude",
70
+ provider="claude",
71
+ error_code="MISSING_API_KEY",
72
+ )
73
+ elif self.ai_provider_name == "openai":
74
+ key = os.getenv("OPENAI_API_KEY")
75
+ if not key:
76
+ raise AIProviderError(
77
+ "OPENAI_API_KEY environment variable required for OpenAI",
78
+ provider="openai",
79
+ error_code="MISSING_API_KEY",
80
+ )
81
+ else:
82
+ raise AIProviderError(
83
+ f"Unsupported AI provider: {self.ai_provider_name}",
84
+ provider=self.ai_provider_name,
85
+ error_code="UNSUPPORTED_PROVIDER",
86
+ )
87
+ return key
88
+
89
+ def _create_ai_provider(self) -> BaseAIProvider:
90
+ """Create the appropriate AI provider."""
91
+ if self.ai_provider_name == "claude":
92
+ return ClaudeProvider(api_key=self.api_key)
93
+ elif self.ai_provider_name == "openai":
94
+ return OpenAIProvider(api_key=self.api_key)
95
+ else:
96
+ raise AIProviderError(
97
+ f"Unsupported AI provider: {self.ai_provider_name}",
98
+ provider=self.ai_provider_name,
99
+ error_code="UNSUPPORTED_PROVIDER",
100
+ )
101
+
102
+ @with_correlation_id
103
+ @with_operation_context(operation="connect_to_servers")
104
+ async def connect_to_servers(self) -> Result[Dict[str, bool]]:
105
+ """Connect to all configured servers."""
106
+ self.connection_pool = await get_connection_pool()
107
+ connection_results = {}
108
+
109
+ for server_name, server_config in self.servers.items():
110
+ try:
111
+ logger.info(f"Connecting to {server_name}")
112
+
113
+ # Register server with connection pool
114
+ if self.connection_pool is not None:
115
+ await self.connection_pool.register_server(
116
+ server_name, server_config
117
+ )
118
+
119
+ # Test connection by getting tools
120
+ async with self.connection_pool.get_connection(
121
+ server_name
122
+ ) as client:
123
+ tools_result = await client.list_tools()
124
+
125
+ # Handle different response formats
126
+ if hasattr(tools_result, "tools"):
127
+ mcp_tools = tools_result.tools
128
+ elif isinstance(tools_result, list):
129
+ mcp_tools = tools_result
130
+ else:
131
+ mcp_tools = []
132
+
133
+ # Store tools
134
+ for mcp_tool in mcp_tools:
135
+ self.available_tools[mcp_tool.name] = (
136
+ mcp_tool,
137
+ server_name,
138
+ )
139
+ logger.debug(
140
+ f"Added tool {mcp_tool.name} from {server_name}"
141
+ )
142
+
143
+ self.connected_servers[server_name] = server_config
144
+ connection_results[server_name] = True
145
+ logger.info(f"Successfully connected to {server_name}")
146
+ else:
147
+ logger.error("Connection pool is None")
148
+ connection_results[server_name] = False
149
+
150
+ except Exception as e:
151
+ logger.error(f"Failed to connect to {server_name}", error=e)
152
+ connection_results[server_name] = False
153
+
154
+ # Update tool executor with available tools
155
+ if self.connection_pool is not None:
156
+ await self.tool_executor.update_tools(
157
+ self.available_tools, self.connection_pool
158
+ )
159
+
160
+ successful_connections = sum(
161
+ 1 for success in connection_results.values() if success
162
+ )
163
+ logger.info(
164
+ f"Connected to {successful_connections}/{len(self.servers)} servers"
165
+ )
166
+
167
+ return Result(status=OperationStatus.SUCCESS, data=connection_results)
168
+
169
+ @with_correlation_id
170
+ @with_operation_context(operation="start_conversation")
171
+ async def start_conversation(
172
+ self,
173
+ initial_message: Optional[str] = None,
174
+ max_steps: Optional[int] = None,
175
+ session_id: Optional[str] = None,
176
+ ) -> Result[ConversationSession]:
177
+ """Start a new conversation session."""
178
+ if session_id is None:
179
+ session_id = str(uuid.uuid4())
180
+
181
+ if session_id in self.active_sessions:
182
+ return Result(
183
+ status=OperationStatus.FAILED,
184
+ error=f"Session {session_id} already exists",
185
+ error_code="SESSION_EXISTS",
186
+ )
187
+
188
+ session = ConversationSession(
189
+ session_id=session_id,
190
+ max_steps=max_steps or self.max_steps,
191
+ ai_provider=self.ai_provider,
192
+ tool_executor=self.tool_executor,
193
+ available_tools=self.available_tools,
194
+ )
195
+
196
+ self.active_sessions[session_id] = session
197
+
198
+ # If initial message provided, process it
199
+ if initial_message:
200
+ result = await session.process_message(initial_message)
201
+ if not result.is_success:
202
+ # Clean up session on failure
203
+ del self.active_sessions[session_id]
204
+ return Result(
205
+ status=OperationStatus.FAILED,
206
+ error=f"Failed to process initial message: {result.error}",
207
+ error_code="INITIAL_MESSAGE_FAILED",
208
+ )
209
+
210
+ logger.info(f"Started conversation session {session_id}")
211
+ return Result(status=OperationStatus.SUCCESS, data=session)
212
+
213
+ @with_correlation_id
214
+ @with_operation_context(operation="chat")
215
+ async def chat(
216
+ self,
217
+ message: str,
218
+ session_id: Optional[str] = None,
219
+ max_steps: Optional[int] = None,
220
+ ) -> Result[ConversationResult]:
221
+ """Send a message and get a complete conversation result."""
222
+ # Create session if none provided
223
+ if session_id is None:
224
+ session_result = await self.start_conversation(max_steps=max_steps)
225
+ if not session_result.is_success:
226
+ return Result(
227
+ status=OperationStatus.FAILED,
228
+ error=f"Failed to create session: {session_result.error}",
229
+ error_code="SESSION_CREATION_FAILED",
230
+ )
231
+ session = session_result.data
232
+ session_id = session.session_id
233
+ else:
234
+ session = self.active_sessions.get(session_id)
235
+ if not session:
236
+ return Result(
237
+ status=OperationStatus.FAILED,
238
+ error=f"Session {session_id} not found",
239
+ error_code="SESSION_NOT_FOUND",
240
+ )
241
+
242
+ # Process the message
243
+ result = await session.process_message(message)
244
+ if not result.is_success:
245
+ return Result(
246
+ status=OperationStatus.FAILED,
247
+ error=f"Failed to process message: {result.error}",
248
+ error_code="MESSAGE_PROCESSING_FAILED",
249
+ )
250
+
251
+ # Create conversation result
252
+ conversation_result = ConversationResult(
253
+ session_id=session_id,
254
+ steps=session.steps.copy(),
255
+ total_duration_ms=sum(step.duration_ms or 0 for step in session.steps),
256
+ )
257
+
258
+ return Result(status=OperationStatus.SUCCESS, data=conversation_result)
259
+
260
+ @with_correlation_id
261
+ async def continue_conversation(
262
+ self, session_id: str, message: str
263
+ ) -> Result[ConversationResult]:
264
+ """Continue an existing conversation."""
265
+ return await self.chat(message, session_id=session_id)
266
+
267
+ async def get_conversation_history(
268
+ self, session_id: str
269
+ ) -> Result[List[ConversationStep]]:
270
+ """Get the conversation history for a session."""
271
+ session = self.active_sessions.get(session_id)
272
+ if not session:
273
+ return Result(
274
+ status=OperationStatus.FAILED,
275
+ error=f"Session {session_id} not found",
276
+ error_code="SESSION_NOT_FOUND",
277
+ )
278
+
279
+ return Result(status=OperationStatus.SUCCESS, data=session.steps.copy())
280
+
281
+ async def execute_tools(
282
+ self, tool_calls: List[ToolCall]
283
+ ) -> Result[List[ToolResult]]:
284
+ """Execute a list of tool calls."""
285
+ if not tool_calls:
286
+ return Result(status=OperationStatus.SUCCESS, data=[])
287
+
288
+ results = await self.tool_executor.execute_tools_concurrently(tool_calls)
289
+
290
+ return Result(status=OperationStatus.SUCCESS, data=results)
291
+
292
+ def get_connected_servers(self) -> List[str]:
293
+ """Get list of connected server names."""
294
+ return list(self.connected_servers.keys())
295
+
296
+ def get_available_tools(self) -> Dict[str, List[str]]:
297
+ """Get all available tools organized by server."""
298
+ tools_by_server: Dict[str, List[str]] = {}
299
+ for tool_name, (mcp_tool, server_name) in self.available_tools.items():
300
+ if server_name not in tools_by_server:
301
+ tools_by_server[server_name] = []
302
+ tools_by_server[server_name].append(tool_name)
303
+ return tools_by_server
304
+
305
+ def find_tool_server(self, tool_name: str) -> Optional[str]:
306
+ """Find which server has a specific tool."""
307
+ if tool_name in self.available_tools:
308
+ return self.available_tools[tool_name][1]
309
+ return None
310
+
311
+ def get_server_status(self) -> Dict[str, Dict[str, Any]]:
312
+ """Get status information for all servers."""
313
+ status = {}
314
+ for server_name in self.connected_servers:
315
+ server_tools = [
316
+ tool
317
+ for tool, (_, srv) in self.available_tools.items()
318
+ if srv == server_name
319
+ ]
320
+ status[server_name] = {
321
+ "connected": True,
322
+ "tools_count": len(server_tools),
323
+ "tools": server_tools,
324
+ }
325
+ return status
326
+
327
+ def get_active_sessions(self) -> Dict[str, ConversationSession]:
328
+ """Get all active conversation sessions."""
329
+ return self.active_sessions.copy()
330
+
331
+ async def close_session(self, session_id: str) -> Result[None]:
332
+ """Close a conversation session."""
333
+ if session_id not in self.active_sessions:
334
+ return Result(
335
+ status=OperationStatus.FAILED,
336
+ error=f"Session {session_id} not found",
337
+ error_code="SESSION_NOT_FOUND",
338
+ )
339
+
340
+ session = self.active_sessions[session_id]
341
+ await session.close()
342
+ del self.active_sessions[session_id]
343
+
344
+ logger.info(f"Closed conversation session {session_id}")
345
+ return Result(status=OperationStatus.SUCCESS)
346
+
347
+ async def disconnect_from_servers(self) -> Result[None]:
348
+ """Disconnect from all servers and clean up resources."""
349
+ logger.info("Disconnecting from all servers...")
350
+
351
+ # Close all active sessions
352
+ for session_id in list(self.active_sessions.keys()):
353
+ await self.close_session(session_id)
354
+
355
+ # Clear server and tool data
356
+ self.connected_servers.clear()
357
+ self.available_tools.clear()
358
+
359
+ # Shutdown connection pool if we have one
360
+ if self.connection_pool:
361
+ from tools.common.async_utils import shutdown_connection_pool
362
+
363
+ await shutdown_connection_pool()
364
+ self.connection_pool = None
365
+
366
+ logger.info("Disconnected from all servers")
367
+ return Result(status=OperationStatus.SUCCESS)
368
+
369
+
370
+ async def create_conversation_client(
371
+ servers: Dict[str, Dict[str, Any]],
372
+ ai_provider: str = "claude",
373
+ api_key: Optional[str] = None,
374
+ max_steps: int = 5,
375
+ ) -> Result[ConversationClient]:
376
+ """Create and connect a conversation client from configuration."""
377
+ try:
378
+ client = ConversationClient(
379
+ servers=servers,
380
+ ai_provider=ai_provider,
381
+ api_key=api_key,
382
+ max_steps=max_steps,
383
+ )
384
+
385
+ connection_result = await client.connect_to_servers()
386
+ if not connection_result.is_success:
387
+ return Result(
388
+ status=OperationStatus.FAILED,
389
+ error=f"Failed to connect to servers: {connection_result.error}",
390
+ error_code="CONNECTION_FAILED",
391
+ )
392
+
393
+ return Result(status=OperationStatus.SUCCESS, data=client)
394
+
395
+ except Exception as e:
396
+ logger.error("Failed to create conversation client", error=e)
397
+ return Result(
398
+ status=OperationStatus.FAILED, error=str(e), error_code=type(e).__name__
399
+ )