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.
- common/__init__.py +21 -0
- common/types.py +182 -0
- lightfast_mcp/__init__.py +50 -0
- lightfast_mcp/core/__init__.py +14 -0
- lightfast_mcp/core/base_server.py +205 -0
- lightfast_mcp/exceptions.py +55 -0
- lightfast_mcp/servers/__init__.py +1 -0
- lightfast_mcp/servers/blender/__init__.py +5 -0
- lightfast_mcp/servers/blender/server.py +358 -0
- lightfast_mcp/servers/blender_mcp_server.py +82 -0
- lightfast_mcp/servers/mock/__init__.py +5 -0
- lightfast_mcp/servers/mock/server.py +101 -0
- lightfast_mcp/servers/mock/tools.py +161 -0
- lightfast_mcp/servers/mock_server.py +78 -0
- lightfast_mcp/utils/__init__.py +1 -0
- lightfast_mcp/utils/logging_utils.py +69 -0
- mseep_lightfast_mcp-0.0.1.dist-info/METADATA +36 -0
- mseep_lightfast_mcp-0.0.1.dist-info/RECORD +43 -0
- mseep_lightfast_mcp-0.0.1.dist-info/WHEEL +5 -0
- mseep_lightfast_mcp-0.0.1.dist-info/entry_points.txt +7 -0
- mseep_lightfast_mcp-0.0.1.dist-info/licenses/LICENSE +21 -0
- mseep_lightfast_mcp-0.0.1.dist-info/top_level.txt +3 -0
- tools/__init__.py +46 -0
- tools/ai/__init__.py +8 -0
- tools/ai/conversation_cli.py +345 -0
- tools/ai/conversation_client.py +399 -0
- tools/ai/conversation_session.py +342 -0
- tools/ai/providers/__init__.py +11 -0
- tools/ai/providers/base_provider.py +64 -0
- tools/ai/providers/claude_provider.py +200 -0
- tools/ai/providers/openai_provider.py +204 -0
- tools/ai/tool_executor.py +257 -0
- tools/common/__init__.py +99 -0
- tools/common/async_utils.py +419 -0
- tools/common/errors.py +222 -0
- tools/common/logging.py +252 -0
- tools/common/types.py +130 -0
- tools/orchestration/__init__.py +15 -0
- tools/orchestration/cli.py +320 -0
- tools/orchestration/config_loader.py +348 -0
- tools/orchestration/server_orchestrator.py +466 -0
- tools/orchestration/server_registry.py +187 -0
- 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
|
+
)
|