@createlex/createlexgenai 1.0.0

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.
@@ -0,0 +1,768 @@
1
+ #!/usr/bin/env python3
2
+ import asyncio
3
+ import inspect
4
+ import json
5
+ import logging
6
+ import websockets
7
+ from typing import Any, Callable, Dict, List, Optional, Union, get_type_hints
8
+ import time
9
+ from datetime import datetime
10
+ import traceback
11
+ import sys
12
+ import os
13
+
14
+ # Import activity tracker for usage logging
15
+ try:
16
+ from activity_tracker import track_tool_call as log_tool_activity, shutdown_tracker
17
+ ACTIVITY_TRACKING_ENABLED = True
18
+ except ImportError:
19
+ ACTIVITY_TRACKING_ENABLED = False
20
+ def log_tool_activity(*args, **kwargs):
21
+ pass
22
+ def shutdown_tracker():
23
+ pass
24
+
25
+ # Configure logging
26
+ logging.basicConfig(
27
+ level=logging.INFO,
28
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
29
+ )
30
+ logger = logging.getLogger('fastmcp')
31
+
32
+ # Connection stability configuration
33
+ CONNECTION_STABILITY_CONFIG = {
34
+ 'max_response_size': 32 * 1024, # 32KB
35
+ 'chunk_size': 16 * 1024, # 16KB
36
+ 'max_chunks': 10,
37
+ 'websocket_max_size': 1024 * 1024, # 1MB
38
+ 'ping_interval': 20,
39
+ 'ping_timeout': 10,
40
+ 'close_timeout': 10
41
+ }
42
+
43
+ def should_summarize_response(data: Any) -> bool:
44
+ """Check if response should be summarized due to size."""
45
+ try:
46
+ json_str = json.dumps(data)
47
+ return len(json_str.encode('utf-8')) > CONNECTION_STABILITY_CONFIG['max_response_size']
48
+ except Exception:
49
+ return False
50
+
51
+ def summarize_large_response(data: Any, tool_name: str = None) -> Any:
52
+ """Create a summary of large response data."""
53
+ try:
54
+ if isinstance(data, dict):
55
+ # Check for blueprint context data
56
+ if data.get('context_type') == 'blueprint_editor':
57
+ return summarize_blueprint_context(data)
58
+ # Check for scene objects data
59
+ elif 'actors' in data and isinstance(data['actors'], list):
60
+ return summarize_scene_objects(data)
61
+ # Generic dict summarization
62
+ else:
63
+ return {
64
+ 'type': 'large_response_summary',
65
+ 'original_keys': list(data.keys())[:10],
66
+ 'total_keys': len(data.keys()) if hasattr(data, 'keys') else 0,
67
+ 'note': f'Large response summarized for {tool_name or "unknown tool"}. Use specific queries for details.'
68
+ }
69
+ elif isinstance(data, list):
70
+ return {
71
+ 'type': 'large_list_summary',
72
+ 'length': len(data),
73
+ 'first_items': data[:3] if len(data) > 0 else [],
74
+ 'note': f'Large list with {len(data)} items summarized. Use specific queries for details.'
75
+ }
76
+ else:
77
+ return {
78
+ 'type': 'large_data_summary',
79
+ 'data_type': type(data).__name__,
80
+ 'note': 'Large response summarized due to size constraints.'
81
+ }
82
+ except Exception as e:
83
+ logger.error(f"Error summarizing response: {e}")
84
+ return {
85
+ 'error': 'Failed to summarize large response',
86
+ 'original_type': type(data).__name__,
87
+ 'note': 'Response was too large to process safely.'
88
+ }
89
+
90
+ def summarize_blueprint_context(data: dict) -> dict:
91
+ """Summarize blueprint context data."""
92
+ summary = {
93
+ 'success': data.get('success', False),
94
+ 'context_type': data.get('context_type'),
95
+ 'has_active_editor': data.get('has_active_editor', False),
96
+ 'note': 'Blueprint context summarized. Use specific queries for detailed information.'
97
+ }
98
+
99
+ if 'active_blueprint' in data:
100
+ summary['active_blueprint'] = data['active_blueprint']
101
+
102
+ if 'editor_state' in data:
103
+ editor_state = data['editor_state']
104
+ summary['editor_summary'] = {
105
+ 'blueprint_name': editor_state.get('blueprint_name'),
106
+ 'parent_class': editor_state.get('parent_class'),
107
+ 'is_compiled': editor_state.get('is_compiled'),
108
+ 'has_compile_errors': editor_state.get('has_compile_errors'),
109
+ 'total_function_graphs': editor_state.get('total_function_graphs')
110
+ }
111
+
112
+ if 'graph_info' in data and 'function_graphs' in data['graph_info']:
113
+ graphs = data['graph_info']['function_graphs']
114
+ summary['function_graphs_summary'] = [
115
+ {
116
+ 'name': g.get('name'),
117
+ 'type': g.get('type'),
118
+ 'node_count': g.get('node_count'),
119
+ 'is_active': g.get('is_active')
120
+ }
121
+ for g in graphs[:5] # First 5 graphs
122
+ ]
123
+ if len(graphs) > 5:
124
+ summary['function_graphs_summary'].append({
125
+ 'note': f'... and {len(graphs) - 5} more graphs'
126
+ })
127
+
128
+ if 'available_nodes' in data:
129
+ available_nodes = data['available_nodes']
130
+ summary['available_nodes_summary'] = {
131
+ 'categories': list(available_nodes.keys()),
132
+ 'total_nodes': sum(len(nodes) for nodes in available_nodes.values()) if isinstance(available_nodes, dict) else 0
133
+ }
134
+
135
+ return summary
136
+
137
+ def summarize_scene_objects(data: dict) -> dict:
138
+ """Summarize scene objects data."""
139
+ actors = data.get('actors', [])
140
+
141
+ # Count actors by class
142
+ class_counts = {}
143
+ for actor in actors:
144
+ actor_class = actor.get('class', 'Unknown')
145
+ class_counts[actor_class] = class_counts.get(actor_class, 0) + 1
146
+
147
+ return {
148
+ 'success': data.get('success', False),
149
+ 'total_actors': len(actors),
150
+ 'actor_classes': class_counts,
151
+ 'sample_actors': [
152
+ {
153
+ 'name': actor.get('name'),
154
+ 'class': actor.get('class'),
155
+ 'location': actor.get('location')
156
+ }
157
+ for actor in actors[:3] # First 3 actors
158
+ ],
159
+ 'note': f'Scene with {len(actors)} actors summarized. Use specific queries for detailed information.'
160
+ }
161
+
162
+ class FastMCP:
163
+ """
164
+ Fast MCP (Model Control Protocol) implementation.
165
+
166
+ This class provides a lightweight framework for defining tools that can be
167
+ exposed to language models for execution, following the Model Control Protocol.
168
+ """
169
+
170
+ def __init__(self, name: str = "MCP"):
171
+ """
172
+ Initialize a new FastMCP instance.
173
+
174
+ Args:
175
+ name: Name of the MCP server
176
+ """
177
+ self.name = name
178
+ self.tools = {}
179
+ self.connected_clients = {}
180
+ self.ws_server = None
181
+
182
+ logger.info(f"Initialized FastMCP: {name}")
183
+
184
+ def tool(self, func=None, **kwargs):
185
+ """
186
+ Decorator to register a function as an MCP tool.
187
+
188
+ Args:
189
+ func: The function to register
190
+ **kwargs: Additional tool metadata
191
+
192
+ Returns:
193
+ The decorated function
194
+ """
195
+ def decorator(f):
196
+ # Get parameter types and return type using type hints
197
+ sig = inspect.signature(f)
198
+ type_hints = get_type_hints(f)
199
+ return_type = type_hints.get('return', Any).__name__
200
+
201
+ # Build a parameter schema
202
+ parameters = []
203
+ for name, param in sig.parameters.items():
204
+ param_type = type_hints.get(name, Any).__name__
205
+ default = param.default if param.default is not inspect.Parameter.empty else None
206
+ has_default = param.default is not inspect.Parameter.empty
207
+
208
+ parameters.append({
209
+ 'name': name,
210
+ 'type': param_type,
211
+ 'required': not has_default,
212
+ 'default': default
213
+ })
214
+
215
+ # Get description from docstring
216
+ description = f.__doc__ or "No description provided"
217
+
218
+ # Convert parameters to MCP schema format
219
+ properties = {}
220
+ required = []
221
+
222
+ for param in parameters:
223
+ param_name = param['name']
224
+ param_type = param['type']
225
+
226
+ # Map Python types to JSON schema types
227
+ if param_type in ['int', 'integer']:
228
+ schema_type = "integer"
229
+ elif param_type in ['float', 'number']:
230
+ schema_type = "number"
231
+ elif param_type in ['bool', 'boolean']:
232
+ schema_type = "boolean"
233
+ elif param_type in ['list', 'array']:
234
+ schema_type = "array"
235
+ elif param_type in ['dict', 'object']:
236
+ schema_type = "object"
237
+ else:
238
+ schema_type = "string"
239
+
240
+ # For arrays, include items schema (required by Gemini API)
241
+ if schema_type == "array":
242
+ properties[param_name] = {"type": schema_type, "items": {"type": "number"}}
243
+ else:
244
+ properties[param_name] = {"type": schema_type}
245
+
246
+ if param['required']:
247
+ required.append(param_name)
248
+
249
+ schema = {
250
+ "type": "object",
251
+ "properties": properties,
252
+ "required": required
253
+ }
254
+
255
+ # Register the tool
256
+ tool_name = kwargs.get('name', f.__name__)
257
+ self.tools[tool_name] = {
258
+ 'function': f,
259
+ 'name': tool_name,
260
+ 'description': description,
261
+ 'parameters': parameters,
262
+ 'schema': schema,
263
+ 'return_type': return_type
264
+ }
265
+
266
+ logger.info(f"Registered tool: {tool_name}")
267
+ return f
268
+
269
+ if func is None:
270
+ return decorator
271
+ return decorator(func)
272
+
273
+ def get_tools_description(self) -> List[Dict]:
274
+ """
275
+ Get a list of tool descriptions.
276
+
277
+ Returns:
278
+ List of tool metadata dictionaries
279
+ """
280
+ tools_info = []
281
+ for name, tool in self.tools.items():
282
+ tools_info.append({
283
+ 'name': name,
284
+ 'description': tool['description'],
285
+ 'parameters': tool['parameters'],
286
+ 'return_type': tool['return_type']
287
+ })
288
+ return tools_info
289
+
290
+ def call_tool(self, name: str, arguments: Dict[str, Any]) -> Any:
291
+ """
292
+ Call a registered tool by name with arguments.
293
+
294
+ Args:
295
+ name: Name of the tool to call
296
+ arguments: Dictionary of arguments to pass to the tool
297
+
298
+ Returns:
299
+ Result of the tool execution
300
+
301
+ Raises:
302
+ ValueError: If the tool is not found
303
+ """
304
+ start_time = time.time()
305
+ success = False
306
+ error_msg = None
307
+ result_summary = None
308
+
309
+ if name not in self.tools:
310
+ logger.error(f"Tool not found: {name}")
311
+ # Track failed tool call
312
+ if ACTIVITY_TRACKING_ENABLED:
313
+ log_tool_activity(
314
+ tool_name=name,
315
+ arguments=arguments,
316
+ success=False,
317
+ duration_ms=0,
318
+ error=f"Tool not found: {name}"
319
+ )
320
+ raise ValueError(f"Tool not found: {name}")
321
+
322
+ tool = self.tools[name]
323
+ func = tool['function']
324
+
325
+ logger.info(f"Calling tool: {name} with arguments: {arguments}")
326
+
327
+ try:
328
+ # Filter arguments to only include those accepted by the function
329
+ import inspect
330
+ sig = inspect.signature(func)
331
+ filtered_args = {k: v for k, v in arguments.items() if k in sig.parameters}
332
+
333
+ # Execute the tool with the filtered arguments
334
+ result = func(**filtered_args)
335
+ logger.info(f"Tool {name} execution result: {result}")
336
+
337
+ success = True
338
+ # Create a brief result summary for analytics
339
+ if isinstance(result, dict):
340
+ result_summary = f"dict with {len(result)} keys"
341
+ elif isinstance(result, list):
342
+ result_summary = f"list with {len(result)} items"
343
+ elif isinstance(result, str):
344
+ result_summary = f"string ({len(result)} chars)"
345
+ else:
346
+ result_summary = type(result).__name__
347
+
348
+ return result
349
+ except Exception as e:
350
+ error_msg = str(e)
351
+ logger.error(f"Error executing tool {name}: {e}")
352
+ logger.error(traceback.format_exc())
353
+ raise
354
+ finally:
355
+ # Track the tool call (success or failure)
356
+ duration_ms = (time.time() - start_time) * 1000
357
+ if ACTIVITY_TRACKING_ENABLED:
358
+ log_tool_activity(
359
+ tool_name=name,
360
+ arguments=arguments,
361
+ success=success,
362
+ duration_ms=duration_ms,
363
+ error=error_msg,
364
+ result_summary=result_summary
365
+ )
366
+
367
+ def run(self, host: str = 'localhost', port: int = 8765) -> None:
368
+ """
369
+ Start the MCP server.
370
+
371
+ Args:
372
+ host: Host to bind the server to (use 'stdio' for stdin/stdout mode)
373
+ port: Port to listen on (ignored in stdio mode)
374
+ """
375
+ if host == 'stdio':
376
+ logger.info(f"Starting FastMCP server: {self.name} in stdio mode")
377
+ asyncio.run(self._run_stdio_server())
378
+ else:
379
+ logger.info(f"Starting FastMCP server: {self.name} on {host}:{port}")
380
+ asyncio.run(self._run_server(host, port))
381
+
382
+ async def _run_server(self, host: str, port: int) -> None:
383
+ """
384
+ Internal method to run the WebSocket server with enhanced stability.
385
+
386
+ Args:
387
+ host: Host to bind the server to
388
+ port: Port to listen on
389
+ """
390
+ # Configure WebSocket server with stability settings
391
+ self.ws_server = await websockets.serve(
392
+ self._handle_client,
393
+ host,
394
+ port,
395
+ max_size=CONNECTION_STABILITY_CONFIG['websocket_max_size'],
396
+ max_queue=32,
397
+ ping_interval=CONNECTION_STABILITY_CONFIG['ping_interval'],
398
+ ping_timeout=CONNECTION_STABILITY_CONFIG['ping_timeout'],
399
+ close_timeout=CONNECTION_STABILITY_CONFIG['close_timeout']
400
+ )
401
+ logger.info(f"WebSocket server started on {host}:{port} with enhanced stability settings")
402
+
403
+ await self.ws_server.wait_closed()
404
+
405
+ async def _run_stdio_server(self) -> None:
406
+ """
407
+ Run the MCP server in stdio mode for Claude Desktop/Cursor integration.
408
+ Windows-compatible version using synchronous I/O.
409
+ """
410
+ import sys
411
+ import threading
412
+ import queue
413
+
414
+ logger.info("FastMCP server running in stdio mode")
415
+
416
+ # Use a queue for thread-safe communication
417
+ input_queue = queue.Queue()
418
+
419
+ def read_stdin():
420
+ """Read from stdin in a separate thread"""
421
+ try:
422
+ while True:
423
+ line = sys.stdin.readline()
424
+ if not line:
425
+ break
426
+ input_queue.put(line.strip())
427
+ except Exception as e:
428
+ logger.error(f"Error reading stdin: {e}")
429
+ input_queue.put(None)
430
+
431
+ # Start stdin reader thread
432
+ stdin_thread = threading.Thread(target=read_stdin, daemon=True)
433
+ stdin_thread.start()
434
+
435
+ try:
436
+ while True:
437
+ try:
438
+ # Get message from queue (blocking)
439
+ message = input_queue.get(timeout=1.0)
440
+ if message is None:
441
+ break
442
+
443
+ if not message:
444
+ continue
445
+
446
+ # Parse the JSON message
447
+ data = json.loads(message)
448
+
449
+ # Process the MCP message
450
+ response = await self._process_mcp_message(data)
451
+
452
+ if response:
453
+ # Send response to stdout
454
+ response_json = json.dumps(response)
455
+ print(response_json, flush=True)
456
+
457
+ except queue.Empty:
458
+ # Timeout - continue loop to check for shutdown
459
+ continue
460
+ except json.JSONDecodeError as e:
461
+ logger.error(f"Invalid JSON received: {e}")
462
+ continue
463
+ except Exception as e:
464
+ logger.error(f"Error processing stdio message: {e}")
465
+ error_response = {
466
+ "jsonrpc": "2.0",
467
+ "id": data.get("id") if 'data' in locals() else None,
468
+ "error": {
469
+ "code": -32603,
470
+ "message": str(e)
471
+ }
472
+ }
473
+ print(json.dumps(error_response), flush=True)
474
+
475
+ except KeyboardInterrupt:
476
+ logger.info("Stdio server interrupted")
477
+ except Exception as e:
478
+ logger.error(f"Stdio server error: {e}")
479
+ finally:
480
+ logger.info("Stdio server shutting down")
481
+ # Shutdown activity tracker to flush pending activities
482
+ if ACTIVITY_TRACKING_ENABLED:
483
+ shutdown_tracker()
484
+
485
+ async def _handle_client(self, websocket):
486
+ """
487
+ Handles a connected WebSocket client.
488
+ """
489
+ client_id = id(websocket)
490
+ self.connected_clients[client_id] = websocket
491
+ logger.info(f"Client {client_id} connected")
492
+
493
+ try:
494
+ async for message in websocket:
495
+ try:
496
+ # Parse the message as JSON
497
+ try:
498
+ data = json.loads(message)
499
+ except json.JSONDecodeError:
500
+ logger.error(f"Invalid JSON from client {client_id}: {message}")
501
+ await websocket.send(json.dumps({
502
+ "type": "error",
503
+ "error": "Invalid JSON message format",
504
+ "timestamp": datetime.now().isoformat()
505
+ }))
506
+ continue
507
+
508
+ # Generate a request ID if not provided
509
+ request_id = data.get("id", str(int(time.time() * 1000)))
510
+ if "id" not in data:
511
+ data["id"] = request_id
512
+
513
+ # Process based on message type
514
+ if "type" not in data:
515
+ logger.error(f"Missing 'type' in message from client {client_id}: {data}")
516
+ await websocket.send(json.dumps({
517
+ "id": request_id,
518
+ "type": "error",
519
+ "error": "Missing 'type' in message",
520
+ "timestamp": datetime.now().isoformat()
521
+ }))
522
+ continue
523
+
524
+ message_type = data["type"]
525
+
526
+ # Check for tool request
527
+ if message_type == "tool_request":
528
+ if "tool" not in data or "arguments" not in data:
529
+ logger.error(f"Missing 'tool' or 'arguments' in tool request from client {client_id}: {data}")
530
+ await websocket.send(json.dumps({
531
+ "id": request_id,
532
+ "type": "error",
533
+ "error": "Tool request must include 'tool' and 'arguments'",
534
+ "timestamp": datetime.now().isoformat()
535
+ }))
536
+ continue
537
+
538
+ tool_name = data["tool"]
539
+ arguments = data["arguments"]
540
+
541
+ try:
542
+ result = self.call_tool(tool_name, arguments)
543
+
544
+ # Check if response should be summarized for stability
545
+ if should_summarize_response(result):
546
+ logger.info(f"Large response detected for {tool_name}, creating summary...")
547
+ result = summarize_large_response(result, tool_name)
548
+ logger.info(f"Response summarized for {tool_name}")
549
+
550
+ response = {
551
+ "id": request_id,
552
+ "type": "tool_response",
553
+ "result": result,
554
+ "tool": tool_name,
555
+ "status": "success",
556
+ "timestamp": datetime.now().isoformat()
557
+ }
558
+
559
+ await websocket.send(json.dumps(response))
560
+
561
+ except Exception as e:
562
+ logger.error(f"Error calling tool {tool_name}: {e}")
563
+ await websocket.send(json.dumps({
564
+ "id": request_id,
565
+ "type": "tool_response",
566
+ "tool": tool_name,
567
+ "status": "error",
568
+ "error": str(e),
569
+ "timestamp": datetime.now().isoformat()
570
+ }))
571
+
572
+ # Check for command request
573
+ elif message_type == "command" or message_type == "mcp_command":
574
+ if "command" not in data or "params" not in data:
575
+ logger.error(f"Missing 'command' or 'params' in command request from client {client_id}: {data}")
576
+ await websocket.send(json.dumps({
577
+ "id": request_id,
578
+ "type": "error",
579
+ "error": "Command request must include 'command' and 'params'",
580
+ "timestamp": datetime.now().isoformat()
581
+ }))
582
+ continue
583
+
584
+ command_name = data["command"]
585
+ params = data["params"]
586
+
587
+ try:
588
+ # Try to find a tool with this name
589
+ if command_name in self.tools:
590
+ result = self.call_tool(command_name, params)
591
+
592
+ # Check if response should be summarized for stability
593
+ if should_summarize_response(result):
594
+ logger.info(f"Large command response detected for {command_name}, creating summary...")
595
+ result = summarize_large_response(result, command_name)
596
+ logger.info(f"Command response summarized for {command_name}")
597
+
598
+ await websocket.send(json.dumps({
599
+ "id": request_id,
600
+ "type": "command_response",
601
+ "result": result,
602
+ "command": command_name,
603
+ "status": "success",
604
+ "timestamp": datetime.now().isoformat()
605
+ }))
606
+ else:
607
+ logger.error(f"Command {command_name} not found")
608
+ await websocket.send(json.dumps({
609
+ "id": request_id,
610
+ "type": "command_response",
611
+ "command": command_name,
612
+ "status": "error",
613
+ "error": f"Command '{command_name}' not found",
614
+ "timestamp": datetime.now().isoformat()
615
+ }))
616
+ except Exception as e:
617
+ logger.error(f"Error executing command {command_name}: {e}")
618
+ await websocket.send(json.dumps({
619
+ "id": request_id,
620
+ "type": "command_response",
621
+ "command": command_name,
622
+ "status": "error",
623
+ "error": str(e),
624
+ "timestamp": datetime.now().isoformat()
625
+ }))
626
+
627
+ # Check for tool list request
628
+ elif message_type == "list_tools":
629
+ tools_info = self.get_tools_description()
630
+ await websocket.send(json.dumps({
631
+ "id": request_id,
632
+ "type": "tool_list",
633
+ "tools": tools_info,
634
+ "count": len(tools_info),
635
+ "timestamp": datetime.now().isoformat()
636
+ }))
637
+
638
+ # Unknown message type
639
+ else:
640
+ logger.error(f"Unknown message type from client {client_id}: {message_type}")
641
+ await websocket.send(json.dumps({
642
+ "id": request_id,
643
+ "type": "error",
644
+ "error": f"Unknown message type: {message_type}",
645
+ "timestamp": datetime.now().isoformat()
646
+ }))
647
+ except Exception as e:
648
+ logger.error(f"Error processing message from client {client_id}: {e}")
649
+ logger.error(traceback.format_exc())
650
+ try:
651
+ await websocket.send(json.dumps({
652
+ "type": "error",
653
+ "error": f"Server error: {str(e)}",
654
+ "traceback": traceback.format_exc(),
655
+ "timestamp": datetime.now().isoformat()
656
+ }))
657
+ except:
658
+ logger.error("Failed to send error message to client")
659
+ except websockets.exceptions.ConnectionClosed:
660
+ logger.info(f"Client {client_id} disconnected")
661
+ finally:
662
+ # Clean up the client when they disconnect
663
+ if client_id in self.connected_clients:
664
+ del self.connected_clients[client_id]
665
+
666
+ async def _process_mcp_message(self, data):
667
+ """
668
+ Process MCP protocol messages for stdio mode.
669
+ """
670
+ try:
671
+ # Handle MCP initialization
672
+ if data.get("method") == "initialize":
673
+ return {
674
+ "jsonrpc": "2.0",
675
+ "id": data.get("id"),
676
+ "result": {
677
+ "protocolVersion": "2024-11-05",
678
+ "capabilities": {
679
+ "tools": {}
680
+ },
681
+ "serverInfo": {
682
+ "name": self.name,
683
+ "version": "1.0.0"
684
+ }
685
+ }
686
+ }
687
+
688
+ # Handle tools/list request
689
+ elif data.get("method") == "tools/list":
690
+ tools_list = []
691
+ for tool_name, tool_info in self.tools.items():
692
+ tools_list.append({
693
+ "name": tool_name,
694
+ "description": tool_info.get("description", ""),
695
+ "inputSchema": tool_info.get("schema", {
696
+ "type": "object",
697
+ "properties": {},
698
+ "required": []
699
+ })
700
+ })
701
+
702
+ return {
703
+ "jsonrpc": "2.0",
704
+ "id": data.get("id"),
705
+ "result": {
706
+ "tools": tools_list
707
+ }
708
+ }
709
+
710
+ # Handle tools/call request
711
+ elif data.get("method") == "tools/call":
712
+ params = data.get("params", {})
713
+ tool_name = params.get("name")
714
+ arguments = params.get("arguments", {})
715
+
716
+ if tool_name in self.tools:
717
+ result = self.call_tool(tool_name, arguments)
718
+ return {
719
+ "jsonrpc": "2.0",
720
+ "id": data.get("id"),
721
+ "result": {
722
+ "content": [
723
+ {
724
+ "type": "text",
725
+ "text": str(result)
726
+ }
727
+ ]
728
+ }
729
+ }
730
+ else:
731
+ return {
732
+ "jsonrpc": "2.0",
733
+ "id": data.get("id"),
734
+ "error": {
735
+ "code": -32601,
736
+ "message": f"Tool '{tool_name}' not found"
737
+ }
738
+ }
739
+
740
+ # Handle notifications (no response needed)
741
+ elif "id" not in data:
742
+ return None
743
+
744
+ # Handle malformed requests with None method (ignore them like createlex-bridge)
745
+ elif data.get('method') is None or data.get('method') == 'None':
746
+ return None # Ignore malformed requests instead of returning errors
747
+
748
+ # Unknown method
749
+ else:
750
+ return {
751
+ "jsonrpc": "2.0",
752
+ "id": data.get("id"),
753
+ "error": {
754
+ "code": -32601,
755
+ "message": f"Method '{data.get('method')}' not found"
756
+ }
757
+ }
758
+
759
+ except Exception as e:
760
+ logger.error(f"Error processing MCP message: {e}")
761
+ return {
762
+ "jsonrpc": "2.0",
763
+ "id": data.get("id"),
764
+ "error": {
765
+ "code": -32603,
766
+ "message": str(e)
767
+ }
768
+ }