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,358 @@
1
+ """Blender MCP server implementation using the new modular architecture."""
2
+
3
+ import asyncio
4
+ import json
5
+ import socket
6
+ import time
7
+ from typing import Any, ClassVar
8
+
9
+ from fastmcp import Context
10
+
11
+ from ...core.base_server import BaseServer, ServerConfig
12
+ from ...exceptions import (
13
+ BlenderCommandError,
14
+ BlenderConnectionError,
15
+ BlenderMCPError,
16
+ BlenderResponseError,
17
+ BlenderTimeoutError,
18
+ )
19
+ from ...utils.logging_utils import get_logger
20
+
21
+ logger = get_logger("BlenderMCPServer")
22
+
23
+
24
+ class BlenderConnection:
25
+ """Handles connection to Blender addon."""
26
+
27
+ def __init__(self, host: str = "localhost", port: int = 9876):
28
+ self.host = host
29
+ self.port = port
30
+ self.sock: socket.socket | None = None
31
+
32
+ def connect(self) -> bool:
33
+ """Connect to the Blender addon socket server."""
34
+ if self.sock:
35
+ return True
36
+
37
+ try:
38
+ self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
39
+ self.sock.connect((self.host, self.port))
40
+ logger.info(f"Connected to Blender at {self.host}:{self.port}")
41
+ return True
42
+ except Exception as e:
43
+ logger.error(f"Failed to connect to Blender: {e}")
44
+ self.sock = None
45
+ raise BlenderConnectionError(
46
+ f"Failed to connect to Blender at {self.host}:{self.port}"
47
+ ) from e
48
+
49
+ def disconnect(self):
50
+ """Disconnect from the Blender addon."""
51
+ if self.sock:
52
+ try:
53
+ self.sock.close()
54
+ logger.info("Disconnected from Blender.")
55
+ except Exception as e:
56
+ logger.error(f"Error during disconnect: {e}")
57
+ finally:
58
+ self.sock = None
59
+
60
+ def is_connected(self) -> bool:
61
+ """Check if connected to Blender."""
62
+ return self.sock is not None
63
+
64
+ def send_command(
65
+ self, command_type: str, params: dict[str, Any] | None = None
66
+ ) -> dict[str, Any]:
67
+ """Send a command to Blender and return the response."""
68
+ if not self.sock:
69
+ self.connect()
70
+
71
+ command = {"type": command_type, "params": params or {}}
72
+
73
+ try:
74
+ logger.info(f"Sending command to Blender: {command_type}")
75
+ if not self.sock:
76
+ raise BlenderConnectionError("Connection to Blender failed")
77
+ self.sock.sendall(json.dumps(command).encode("utf-8"))
78
+
79
+ # Receive response
80
+ response_data = self._receive_response()
81
+ response = json.loads(response_data.decode("utf-8"))
82
+
83
+ if response.get("status") == "error":
84
+ error_message = response.get("message", "Unknown error from Blender")
85
+ raise BlenderCommandError(error_message)
86
+
87
+ return response.get("result", {})
88
+
89
+ except json.JSONDecodeError as e:
90
+ raise BlenderResponseError(
91
+ f"Invalid JSON response from Blender: {e}"
92
+ ) from e
93
+ except BlenderMCPError:
94
+ raise
95
+ except Exception as e:
96
+ self.disconnect()
97
+ raise BlenderConnectionError(
98
+ f"Error communicating with Blender: {e}"
99
+ ) from e
100
+
101
+ def _receive_response(self, buffer_size: int = 8192) -> bytes:
102
+ """Receive the complete response from Blender."""
103
+ if not self.sock:
104
+ raise BlenderConnectionError("Not connected to Blender")
105
+
106
+ chunks = []
107
+ self.sock.settimeout(15.0)
108
+
109
+ try:
110
+ while True:
111
+ chunk = self.sock.recv(buffer_size)
112
+ if not chunk:
113
+ break
114
+
115
+ chunks.append(chunk)
116
+
117
+ # Try to parse as complete JSON
118
+ try:
119
+ data_so_far = b"".join(chunks)
120
+ json.loads(data_so_far.decode("utf-8"))
121
+ return data_so_far
122
+ except json.JSONDecodeError:
123
+ continue
124
+
125
+ except TimeoutError:
126
+ raise BlenderTimeoutError("Timeout waiting for Blender response") from None
127
+ except Exception as e:
128
+ raise BlenderConnectionError(f"Error receiving response: {e}") from e
129
+
130
+ if chunks:
131
+ return b"".join(chunks)
132
+ else:
133
+ raise BlenderResponseError("No response received from Blender")
134
+
135
+
136
+ class BlenderMCPServer(BaseServer):
137
+ """Blender MCP server for 3D modeling and animation control."""
138
+
139
+ # Server metadata
140
+ SERVER_TYPE: ClassVar[str] = "blender"
141
+ SERVER_VERSION: ClassVar[str] = "1.0.0"
142
+ REQUIRED_DEPENDENCIES: ClassVar[list[str]] = []
143
+ REQUIRED_APPS: ClassVar[list[str]] = ["Blender"]
144
+
145
+ def __init__(self, config: ServerConfig):
146
+ """Initialize the Blender server."""
147
+ super().__init__(config)
148
+
149
+ # Blender-specific configuration
150
+ blender_host = config.config.get("blender_host", "localhost")
151
+ blender_port = config.config.get("blender_port", 9876)
152
+
153
+ self.blender_connection = BlenderConnection(blender_host, blender_port)
154
+
155
+ logger.info(f"Blender server configured for {blender_host}:{blender_port}")
156
+
157
+ def _register_tools(self):
158
+ """Register Blender server tools."""
159
+ if not self.mcp:
160
+ return
161
+
162
+ # Register tools
163
+ self.mcp.tool()(self.get_state)
164
+ self.mcp.tool()(self.execute_command)
165
+
166
+ # Update available tools list
167
+ self.info.tools = ["get_state", "execute_command"]
168
+ logger.info(f"Registered {len(self.info.tools)} tools: {self.info.tools}")
169
+
170
+ async def _check_application(self, app: str) -> bool:
171
+ """Check if Blender is available."""
172
+ if app.lower() == "blender":
173
+ return await self._check_blender_connection()
174
+ return True
175
+
176
+ async def _check_blender_connection(self) -> bool:
177
+ """Check if Blender is accessible."""
178
+ try:
179
+ # Quick socket check
180
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
181
+ sock.settimeout(2.0)
182
+ result = sock.connect_ex(
183
+ (self.blender_connection.host, self.blender_connection.port)
184
+ )
185
+ sock.close()
186
+ return result == 0
187
+ except Exception:
188
+ return False
189
+
190
+ async def _on_startup(self):
191
+ """Blender server startup logic."""
192
+ logger.info(f"Blender server '{self.config.name}' starting up...")
193
+
194
+ # Test connection to Blender
195
+ try:
196
+ # Try to connect and ping
197
+ connection_available = await self._check_blender_connection()
198
+ if connection_available:
199
+ logger.info("Blender connection test successful")
200
+ else:
201
+ logger.warning(
202
+ "Blender not accessible. Ensure Blender is running with the addon active."
203
+ )
204
+ except Exception as e:
205
+ logger.warning(f"Blender connection test failed: {e}")
206
+
207
+ logger.info("Blender server startup complete")
208
+
209
+ async def _on_shutdown(self):
210
+ """Blender server shutdown logic."""
211
+ logger.info(f"Blender server '{self.config.name}' shutting down...")
212
+
213
+ # Disconnect from Blender
214
+ try:
215
+ self.blender_connection.disconnect()
216
+ except Exception as e:
217
+ logger.error(f"Error during Blender disconnect: {e}")
218
+
219
+ logger.info("Blender server shutdown complete")
220
+
221
+ async def _perform_health_check(self) -> bool:
222
+ """Perform Blender server health check."""
223
+ try:
224
+ # Check if we can reach Blender
225
+ is_reachable = await self._check_blender_connection()
226
+ if not is_reachable:
227
+ return False
228
+
229
+ # Try a simple ping command
230
+ await asyncio.get_event_loop().run_in_executor(
231
+ None, self.blender_connection.send_command, "ping"
232
+ )
233
+ return True
234
+ except Exception as e:
235
+ logger.debug(f"Health check failed: {e}")
236
+ return False
237
+
238
+ # Tool implementations
239
+ async def get_state(self, ctx: Context) -> str:
240
+ """
241
+ Get detailed information about the current Blender scene.
242
+ This corresponds to the 'get_scene_info' command in the Blender addon.
243
+ """
244
+ loop = asyncio.get_event_loop()
245
+
246
+ try:
247
+ logger.info("Executing get_state (get_scene_info) command.")
248
+
249
+ # Run the Blender command in executor to avoid blocking
250
+ result = await loop.run_in_executor(
251
+ None, self.blender_connection.send_command, "get_scene_info"
252
+ )
253
+
254
+ # Add diagnostic information
255
+ result["_connection_info"] = {
256
+ "connected": True,
257
+ "host": self.blender_connection.host,
258
+ "port": self.blender_connection.port,
259
+ "server_name": self.config.name,
260
+ "connection_time": time.time(),
261
+ }
262
+
263
+ return json.dumps(result, indent=2)
264
+
265
+ except BlenderMCPError as e:
266
+ logger.error(f"BlenderMCPError in get_state: {e}")
267
+ return json.dumps(
268
+ {
269
+ "error": f"Blender Interaction Error: {str(e)}",
270
+ "type": type(e).__name__,
271
+ "server_name": self.config.name,
272
+ },
273
+ indent=2,
274
+ )
275
+ except Exception as e:
276
+ logger.error(f"Unexpected error in get_state: {e}")
277
+ return json.dumps(
278
+ {
279
+ "error": f"Unexpected server error: {str(e)}",
280
+ "type": type(e).__name__,
281
+ "server_name": self.config.name,
282
+ },
283
+ indent=2,
284
+ )
285
+
286
+ async def execute_command(self, ctx: Context, code_to_execute: str) -> str:
287
+ """
288
+ Execute arbitrary Python code in Blender.
289
+ This corresponds to the 'execute_code' command in the Blender addon.
290
+
291
+ Parameters:
292
+ - code_to_execute: The Python code string to execute in Blender's context.
293
+ """
294
+ loop = asyncio.get_event_loop()
295
+
296
+ try:
297
+ logger.info(f"Executing command with code: {code_to_execute[:100]}...")
298
+
299
+ # Run the Blender command in executor
300
+ result = await loop.run_in_executor(
301
+ None,
302
+ self.blender_connection.send_command,
303
+ "execute_code",
304
+ {"code": code_to_execute},
305
+ )
306
+
307
+ # Add server info to result
308
+ result["_server_info"] = {
309
+ "server_name": self.config.name,
310
+ "server_type": self.SERVER_TYPE,
311
+ "execution_time": time.time(),
312
+ }
313
+
314
+ return json.dumps(result, indent=2)
315
+
316
+ except BlenderMCPError as e:
317
+ logger.error(f"BlenderMCPError in execute_command: {e}")
318
+ return json.dumps(
319
+ {
320
+ "error": f"Blender Command Execution Error: {str(e)}",
321
+ "type": type(e).__name__,
322
+ "server_name": self.config.name,
323
+ },
324
+ indent=2,
325
+ )
326
+ except Exception as e:
327
+ logger.error(f"Unexpected error in execute_command: {e}")
328
+ return json.dumps(
329
+ {
330
+ "error": f"Unexpected server error during command execution: {str(e)}",
331
+ "type": type(e).__name__,
332
+ "server_name": self.config.name,
333
+ },
334
+ indent=2,
335
+ )
336
+
337
+
338
+ def main():
339
+ """Run the Blender MCP server directly."""
340
+ # Create a default configuration for standalone running
341
+ config = ServerConfig(
342
+ name="BlenderMCP",
343
+ description="Blender MCP Server for 3D modeling and animation",
344
+ config={
345
+ "type": "blender",
346
+ "blender_host": "localhost",
347
+ "blender_port": 9876,
348
+ },
349
+ )
350
+
351
+ # Create and run the server
352
+ server = BlenderMCPServer(config)
353
+ logger.info(f"Starting standalone Blender server: {config.name}")
354
+ server.run()
355
+
356
+
357
+ if __name__ == "__main__":
358
+ main()
@@ -0,0 +1,82 @@
1
+ """
2
+ Blender MCP server using the new modular architecture.
3
+ This is now the clean entry point for the Blender server.
4
+ """
5
+
6
+ import json
7
+ import os
8
+
9
+ from ..core.base_server import ServerConfig
10
+ from ..utils.logging_utils import configure_logging, get_logger
11
+ from .blender.server import BlenderMCPServer
12
+
13
+ # Configure logging
14
+ configure_logging(level="INFO")
15
+ logger = get_logger("BlenderMCP")
16
+
17
+
18
+ def main():
19
+ """Run the Blender MCP server."""
20
+ logger.info("Starting Blender MCP server")
21
+
22
+ # Check for environment configuration (from ServerOrchestrator)
23
+ env_config = os.getenv("LIGHTFAST_MCP_SERVER_CONFIG")
24
+
25
+ if env_config:
26
+ try:
27
+ # Parse configuration from environment
28
+ config_data = json.loads(env_config)
29
+ config = ServerConfig(
30
+ name=config_data.get("name", "BlenderMCP"),
31
+ description=config_data.get(
32
+ "description", "Blender MCP Server for 3D modeling and animation"
33
+ ),
34
+ host=config_data.get("host", "localhost"),
35
+ port=config_data.get("port", 8001),
36
+ transport=config_data.get(
37
+ "transport", "streamable-http"
38
+ ), # Default to HTTP for subprocess
39
+ path=config_data.get("path", "/mcp"),
40
+ config=config_data.get(
41
+ "config",
42
+ {
43
+ "type": "blender",
44
+ "blender_host": "localhost",
45
+ "blender_port": 9876,
46
+ },
47
+ ),
48
+ )
49
+ logger.info(
50
+ f"Using environment configuration: {config.transport}://{config.host}:{config.port}"
51
+ )
52
+ except (json.JSONDecodeError, KeyError) as e:
53
+ logger.warning(f"Invalid environment configuration: {e}, using defaults")
54
+ config = _get_default_config()
55
+ else:
56
+ # Use default configuration for standalone running
57
+ config = _get_default_config()
58
+
59
+ # Create and run server
60
+ server = BlenderMCPServer(config)
61
+ server.run()
62
+
63
+
64
+ def _get_default_config() -> ServerConfig:
65
+ """Get default configuration for standalone running."""
66
+ return ServerConfig(
67
+ name="BlenderMCP",
68
+ description="Blender MCP Server for 3D modeling and animation",
69
+ host="localhost",
70
+ port=8001, # Use port 8001 by default for blender server
71
+ transport="streamable-http", # Use HTTP by default for easier testing
72
+ path="/mcp",
73
+ config={
74
+ "type": "blender",
75
+ "blender_host": "localhost",
76
+ "blender_port": 9876,
77
+ },
78
+ )
79
+
80
+
81
+ if __name__ == "__main__":
82
+ main()
@@ -0,0 +1,5 @@
1
+ """Mock MCP server module."""
2
+
3
+ from .server import MockMCPServer
4
+
5
+ __all__ = ["MockMCPServer"]
@@ -0,0 +1,101 @@
1
+ """Mock MCP server implementation using the new modular architecture."""
2
+
3
+ import asyncio
4
+ from typing import ClassVar
5
+
6
+ from ...core.base_server import BaseServer, ServerConfig
7
+ from ...utils.logging_utils import get_logger
8
+ from . import tools
9
+
10
+ logger = get_logger("MockMCPServer")
11
+
12
+
13
+ class MockMCPServer(BaseServer):
14
+ """Mock MCP server for testing and development."""
15
+
16
+ # Server metadata
17
+ SERVER_TYPE: ClassVar[str] = "mock"
18
+ SERVER_VERSION: ClassVar[str] = "1.0.0"
19
+ REQUIRED_DEPENDENCIES: ClassVar[list[str]] = []
20
+ REQUIRED_APPS: ClassVar[list[str]] = []
21
+
22
+ def __init__(self, config: ServerConfig):
23
+ """Initialize the mock server."""
24
+ super().__init__(config)
25
+
26
+ # Mock-specific configuration with validation
27
+ delay_value = config.config.get("delay_seconds", 0.5)
28
+ try:
29
+ # Try to convert to float and validate
30
+ if delay_value is None or delay_value == "":
31
+ self.default_delay = 0.5
32
+ else:
33
+ self.default_delay = float(delay_value)
34
+ if self.default_delay < 0:
35
+ self.default_delay = 0.5
36
+ except (ValueError, TypeError):
37
+ # If conversion fails, use default
38
+ self.default_delay = 0.5
39
+
40
+ # Set this server instance in tools module for access
41
+ tools.set_current_server(self)
42
+
43
+ def _register_tools(self):
44
+ """Register mock server tools."""
45
+ if not self.mcp:
46
+ return
47
+
48
+ # Register tools from the tools module
49
+ self.mcp.tool()(tools.get_server_status)
50
+ self.mcp.tool()(tools.fetch_mock_data)
51
+ self.mcp.tool()(tools.execute_mock_action)
52
+
53
+ # Update the server info with available tools
54
+ self.info.tools = [
55
+ "get_server_status",
56
+ "fetch_mock_data",
57
+ "execute_mock_action",
58
+ ]
59
+ logger.info(
60
+ "Registered 3 tools: get_server_status, fetch_mock_data, execute_mock_action"
61
+ )
62
+
63
+ async def _on_startup(self):
64
+ """Mock server startup logic."""
65
+ logger.info(f"Mock server '{self.config.name}' starting up...")
66
+ logger.info(f"Default delay configured: {self.default_delay}s")
67
+
68
+ # Simulate some startup work
69
+ await asyncio.sleep(0.1)
70
+
71
+ logger.info("Mock server startup complete")
72
+
73
+ async def _on_shutdown(self):
74
+ """Mock server shutdown logic."""
75
+ logger.info(f"Mock server '{self.config.name}' shutting down...")
76
+ await asyncio.sleep(0.05) # Simulate cleanup
77
+ logger.info("Mock server shutdown complete")
78
+
79
+ async def _perform_health_check(self) -> bool:
80
+ """Perform mock server health check."""
81
+ # Simple health check - just verify we're running
82
+ return self.info.is_running
83
+
84
+
85
+ def main():
86
+ """Run the Mock MCP server directly."""
87
+ # Create a default configuration for standalone running
88
+ config = ServerConfig(
89
+ name="MockMCP",
90
+ description="Mock MCP Server for testing and development",
91
+ config={"type": "mock", "delay_seconds": 0.5},
92
+ )
93
+
94
+ # Create and run the server
95
+ server = MockMCPServer(config)
96
+ logger.info(f"Starting standalone mock server: {config.name}")
97
+ server.run()
98
+
99
+
100
+ if __name__ == "__main__":
101
+ main()