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,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,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()
|