@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.
- package/README.md +272 -0
- package/bin/createlex.js +5 -0
- package/package.json +45 -0
- package/python/activity_tracker.py +280 -0
- package/python/fastmcp.py +768 -0
- package/python/mcp_server_stdio.py +4720 -0
- package/python/requirements.txt +7 -0
- package/python/subscription_validator.py +199 -0
- package/python/ue_native_handler.py +573 -0
- package/python/ui_slice_host.py +637 -0
- package/src/cli.js +109 -0
- package/src/commands/config.js +56 -0
- package/src/commands/connect.js +100 -0
- package/src/commands/exec.js +148 -0
- package/src/commands/login.js +111 -0
- package/src/commands/logout.js +17 -0
- package/src/commands/serve.js +237 -0
- package/src/commands/setup.js +65 -0
- package/src/commands/status.js +126 -0
- package/src/commands/tools.js +133 -0
- package/src/core/auth-manager.js +147 -0
- package/src/core/config-store.js +81 -0
- package/src/core/discovery.js +71 -0
- package/src/core/ide-configurator.js +189 -0
- package/src/core/remote-execution.js +228 -0
- package/src/core/subscription.js +176 -0
- package/src/core/unreal-connection.js +318 -0
- package/src/core/web-remote-control.js +243 -0
- package/src/utils/logger.js +66 -0
- package/src/utils/python-manager.js +142 -0
|
@@ -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
|
+
}
|