@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
package/README.md
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# @createlex/createlexgenai
|
|
2
|
+
|
|
3
|
+
CLI tool for integrating AI-powered tools with Unreal Engine. Works as an MCP server for AI CLI tools (Claude Code, Codex CLI, Gemini CLI, etc.) or as a standalone command-line interface.
|
|
4
|
+
|
|
5
|
+
**71+ tools** for spawning actors, creating Blueprints, managing materials, UI widgets, physics, project architecture, and more — all without requiring a custom UE plugin.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Install globally
|
|
11
|
+
npm install -g @createlex/createlexgenai
|
|
12
|
+
|
|
13
|
+
# Authenticate
|
|
14
|
+
createlex login
|
|
15
|
+
|
|
16
|
+
# Check everything is working
|
|
17
|
+
createlex status
|
|
18
|
+
|
|
19
|
+
# Set up your AI tool
|
|
20
|
+
createlex setup claude-code
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Requirements
|
|
24
|
+
|
|
25
|
+
- **Node.js 18+**
|
|
26
|
+
- **Unreal Engine 5.x** with at least one backend enabled (see below)
|
|
27
|
+
- **Python 3.10+** (for MCP serve mode)
|
|
28
|
+
- **CreateLex account** with active subscription
|
|
29
|
+
|
|
30
|
+
## Setup Guide (No Plugin Required)
|
|
31
|
+
|
|
32
|
+
### Step 1: Install the CLI
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install -g @createlex/createlexgenai
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Or run without installing:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npx @createlex/createlexgenai --version
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Step 2: Authenticate
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
createlex login
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
This opens your browser to sign in. Your token is saved to `~/.createlex/auth.json`.
|
|
51
|
+
|
|
52
|
+
Verify:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
createlex status
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Step 3: Enable UE Built-in Backends
|
|
59
|
+
|
|
60
|
+
You need **at least one** of these enabled in your Unreal project. Both are built into UE — no custom plugin required.
|
|
61
|
+
|
|
62
|
+
#### Option A: Web Remote Control (Recommended)
|
|
63
|
+
|
|
64
|
+
1. Open your UE project
|
|
65
|
+
2. Go to **Edit > Plugins**
|
|
66
|
+
3. Search for **"Web Remote Control"** and enable it
|
|
67
|
+
4. Restart the editor
|
|
68
|
+
5. Go to **Edit > Project Settings > Plugins > Remote Control**
|
|
69
|
+
6. Check **"Enable Remote Control Web Server"**
|
|
70
|
+
7. Set port to **30010** (default)
|
|
71
|
+
8. Ensure **"Enable HTTP Server"** is checked
|
|
72
|
+
|
|
73
|
+
#### Option B: Python Remote Execution
|
|
74
|
+
|
|
75
|
+
1. Open your UE project
|
|
76
|
+
2. Go to **Edit > Plugins**
|
|
77
|
+
3. Search for **"Python Editor Script Plugin"** and enable it
|
|
78
|
+
4. Restart the editor
|
|
79
|
+
5. Go to **Edit > Project Settings > Plugins > Python**
|
|
80
|
+
6. Check **"Enable Remote Execution"**
|
|
81
|
+
7. Multicast group: `239.0.0.1`, port: `6766` (defaults)
|
|
82
|
+
|
|
83
|
+
> **Tip:** Enable both for maximum reliability. If one backend is unavailable, the CLI automatically falls back to the other.
|
|
84
|
+
|
|
85
|
+
### Step 4: Verify Connection
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
createlex connect
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Expected output (without plugin):
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
Plugin (TCP 9878): Not connected
|
|
95
|
+
Web Remote Control (HTTP 30010): Connected
|
|
96
|
+
Remote Execution (UDP 6766): Available
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Step 5: Configure Your AI Tool / IDE
|
|
100
|
+
|
|
101
|
+
#### Automatic Setup
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
# Set up a specific IDE
|
|
105
|
+
createlex setup <ide>
|
|
106
|
+
|
|
107
|
+
# Set up all detected IDEs at once
|
|
108
|
+
createlex setup --all
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
#### Supported IDEs
|
|
112
|
+
|
|
113
|
+
| IDE | Command | Config Location |
|
|
114
|
+
|---|---|---|
|
|
115
|
+
| Claude Code | `createlex setup claude-code` | CLI command (manual) |
|
|
116
|
+
| Cursor | `createlex setup cursor` | `~/.cursor/mcp.json` |
|
|
117
|
+
| Windsurf | `createlex setup windsurf` | `~/.codeium/windsurf/mcp_config.json` |
|
|
118
|
+
| Claude Desktop | `createlex setup claude-desktop` | `%APPDATA%/Claude/claude_desktop_config.json` |
|
|
119
|
+
| VS Code | `createlex setup vscode` | `.vscode/settings.json` |
|
|
120
|
+
| Antigravity | `createlex setup antigravity` | `~/.gemini/antigravity/mcp_config.json` |
|
|
121
|
+
| Gemini CLI | `createlex setup gemini-cli` | `~/.gemini/settings.json` |
|
|
122
|
+
| Kiro | `createlex setup kiro` | `~/.kiro/settings/mcp.json` |
|
|
123
|
+
| Trae | `createlex setup trae` | `%APPDATA%/Trae/User/mcp.json` |
|
|
124
|
+
| Augment | `createlex setup augment` | `~/.augment/mcp.json` |
|
|
125
|
+
| Codex | `createlex setup codex` | `~/.codex/config.toml` |
|
|
126
|
+
|
|
127
|
+
#### Manual IDE Configuration
|
|
128
|
+
|
|
129
|
+
All IDEs use the same MCP server command:
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
npx @createlex/createlexgenai serve
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Claude Code:**
|
|
136
|
+
```bash
|
|
137
|
+
claude mcp add createlex-unreal -- npx @createlex/createlexgenai serve
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**Codex CLI** (`~/.codex/config.toml`):
|
|
141
|
+
```toml
|
|
142
|
+
[mcp_servers.createlex-unreal]
|
|
143
|
+
command = "npx"
|
|
144
|
+
args = ["@createlex/createlexgenai", "serve"]
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**JSON-based IDEs** (Cursor, Windsurf, Claude Desktop, etc.):
|
|
148
|
+
```json
|
|
149
|
+
{
|
|
150
|
+
"mcpServers": {
|
|
151
|
+
"createlex-unreal": {
|
|
152
|
+
"command": "npx",
|
|
153
|
+
"args": ["@createlex/createlexgenai", "serve"]
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Step 6: Start Using It
|
|
160
|
+
|
|
161
|
+
Once configured, your AI tool has access to all 71+ Unreal Engine tools. Try:
|
|
162
|
+
|
|
163
|
+
- *"Spawn a cube at the origin"*
|
|
164
|
+
- *"Create a Blueprint called BP_Enemy based on Character"*
|
|
165
|
+
- *"List all actors in the scene"*
|
|
166
|
+
- *"Create a medieval town"*
|
|
167
|
+
|
|
168
|
+
## CLI Commands
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
# MCP server mode (for AI CLI tools)
|
|
172
|
+
createlex serve # stdio MCP server
|
|
173
|
+
createlex serve --port 38080 # TCP MCP server
|
|
174
|
+
|
|
175
|
+
# Direct tool execution
|
|
176
|
+
createlex exec <tool> [--key value ...]
|
|
177
|
+
createlex exec get_all_scene_objects
|
|
178
|
+
createlex exec spawn_object --actor-class StaticMeshActor --location "0,0,0"
|
|
179
|
+
createlex exec execute_python_script --script "print('Hello from UE!')"
|
|
180
|
+
|
|
181
|
+
# List available tools
|
|
182
|
+
createlex tools # Formatted list
|
|
183
|
+
createlex tools --json # JSON output
|
|
184
|
+
|
|
185
|
+
# Auth
|
|
186
|
+
createlex login # Browser auth flow
|
|
187
|
+
createlex logout # Clear credentials
|
|
188
|
+
|
|
189
|
+
# Status & connection
|
|
190
|
+
createlex status # Auth + subscription + UE connection info
|
|
191
|
+
createlex connect # Test UE connection, show setup tips
|
|
192
|
+
|
|
193
|
+
# Configuration
|
|
194
|
+
createlex config list # Show all settings
|
|
195
|
+
createlex config get unrealPort # Get a specific value
|
|
196
|
+
createlex config set unrealPort 9879
|
|
197
|
+
createlex config reset # Reset to defaults
|
|
198
|
+
|
|
199
|
+
# IDE setup
|
|
200
|
+
createlex setup # List supported IDEs
|
|
201
|
+
createlex setup claude-code # Set up specific IDE
|
|
202
|
+
createlex setup --all # Set up all detected IDEs
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## How Connection Fallback Works
|
|
206
|
+
|
|
207
|
+
When a tool is executed, the CLI tries backends in this order:
|
|
208
|
+
|
|
209
|
+
1. **CreatelexGenAI Plugin** (TCP port 9878) — Fastest, direct JSON. Used if the plugin is installed.
|
|
210
|
+
2. **Web Remote Control** (HTTP port 30010) — Translates commands to Python, executes via UE's built-in HTTP API.
|
|
211
|
+
3. **Python Remote Execution** (UDP port 6766) — Translates commands to Python, executes via UE's multicast remote execution protocol.
|
|
212
|
+
|
|
213
|
+
All 71 tools work through all backends. The `ue_native_handler.py` module translates every command into standalone Python that runs inside UE's interpreter.
|
|
214
|
+
|
|
215
|
+
## Available Tools (71+)
|
|
216
|
+
|
|
217
|
+
| Category | Tools |
|
|
218
|
+
|---|---|
|
|
219
|
+
| **General** | `get_all_scene_objects`, `execute_python_script`, `execute_unreal_command` |
|
|
220
|
+
| **Script** | `execute_python_script`, `run_automation_test` |
|
|
221
|
+
| **Actors** | `spawn_object`, `delete_actor`, `find_actors_by_name`, `set_actor_transform`, `set_actor_property`, `get_actor_properties`, `duplicate_actor`, `rename_actor`, `set_actor_mobility`, `set_actor_physics` |
|
|
222
|
+
| **Blueprint** | `create_blueprint`, `spawn_blueprint`, `compile_blueprint`, `add_component_to_blueprint`, `set_blueprint_variable`, `get_blueprint_details` |
|
|
223
|
+
| **Blueprint Nodes** | `add_node`, `connect_nodes`, `add_variable`, `add_function`, `add_event`, `set_pin_value`, `get_blueprint_graph` |
|
|
224
|
+
| **Materials** | `create_material`, `apply_material_to_actor`, `set_material_parameter`, `create_material_instance` |
|
|
225
|
+
| **Mesh** | `set_static_mesh`, `create_procedural_mesh`, `merge_meshes` |
|
|
226
|
+
| **Physics** | `simulate_physics`, `add_physics_constraint`, `set_collision_profile`, `add_force`, `set_physics_material` |
|
|
227
|
+
| **UI/Widgets** | `create_widget`, `modify_widget`, `bind_widget_event`, `add_widget_animation`, `create_widget_component` |
|
|
228
|
+
| **UI Slicing** | `upload_ui_image`, `slice_ui_image`, `generate_ui_from_slices` |
|
|
229
|
+
| **Project** | `create_project_folder`, `get_files_in_folder`, `import_asset`, `create_level`, `set_level_settings` |
|
|
230
|
+
| **Architecture** | `create_town`, `construct_house`, `construct_building`, `create_road`, `create_landscape`, `place_foliage`, `create_water_body`, `create_sky_atmosphere`, `create_lighting_scenario`, `create_marketplace`, `create_wall`, `create_floor` |
|
|
231
|
+
|
|
232
|
+
## Configuration
|
|
233
|
+
|
|
234
|
+
Settings are stored in `~/.createlex/config.json`:
|
|
235
|
+
|
|
236
|
+
| Key | Default | Description |
|
|
237
|
+
|---|---|---|
|
|
238
|
+
| `unrealPort` | `9878` | CreatelexGenAI plugin TCP port |
|
|
239
|
+
| `mcpPort` | `38080` | MCP TCP server port (for `serve --port`) |
|
|
240
|
+
| `apiBaseUrl` | `https://api.createlex.com/api` | CreateLex API endpoint |
|
|
241
|
+
| `webBaseUrl` | `https://createlex.com` | CreateLex web app URL |
|
|
242
|
+
| `debug` | `false` | Enable debug logging |
|
|
243
|
+
|
|
244
|
+
## Troubleshooting
|
|
245
|
+
|
|
246
|
+
### "No backends connected to Unreal Engine"
|
|
247
|
+
- Make sure UE is running with your project open
|
|
248
|
+
- Enable Web Remote Control or Python Remote Execution (see Step 3)
|
|
249
|
+
- Run `createlex connect` for detailed diagnostics
|
|
250
|
+
|
|
251
|
+
### "Authentication: Not logged in"
|
|
252
|
+
```bash
|
|
253
|
+
createlex login
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### "Subscription: Inactive"
|
|
257
|
+
- Visit [createlex.com](https://createlex.com) to check your subscription status
|
|
258
|
+
- Run `createlex status` to see the specific error
|
|
259
|
+
|
|
260
|
+
### Web Remote Control not responding
|
|
261
|
+
- Check **Edit > Project Settings > Plugins > Remote Control** in UE
|
|
262
|
+
- Ensure "Enable Remote Control Web Server" is checked
|
|
263
|
+
- Verify port 30010 is not blocked by firewall
|
|
264
|
+
|
|
265
|
+
### Python Remote Execution not available
|
|
266
|
+
- Check **Edit > Project Settings > Plugins > Python** in UE
|
|
267
|
+
- Ensure "Enable Remote Execution" is checked
|
|
268
|
+
- Verify UDP port 6766 is not blocked
|
|
269
|
+
|
|
270
|
+
## License
|
|
271
|
+
|
|
272
|
+
Proprietary. Requires an active CreateLex subscription.
|
package/bin/createlex.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@createlex/createlexgenai",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI tool and MCP server for CreatelexGenAI — Unreal Engine AI integration",
|
|
5
|
+
"bin": {
|
|
6
|
+
"createlex": "./bin/createlex.js"
|
|
7
|
+
},
|
|
8
|
+
"main": "src/cli.js",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node bin/createlex.js",
|
|
11
|
+
"test": "echo \"No tests yet\" && exit 0"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"createlex",
|
|
15
|
+
"unreal-engine",
|
|
16
|
+
"mcp",
|
|
17
|
+
"ai",
|
|
18
|
+
"cli",
|
|
19
|
+
"generative-ai",
|
|
20
|
+
"game-development"
|
|
21
|
+
],
|
|
22
|
+
"author": "CreateLex LLC",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18.0.0"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"axios": "^1.6.0",
|
|
29
|
+
"chalk": "^4.1.2",
|
|
30
|
+
"commander": "^12.1.0",
|
|
31
|
+
"open": "^8.4.2",
|
|
32
|
+
"ora": "^5.4.1"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"bin/",
|
|
36
|
+
"src/",
|
|
37
|
+
"python/",
|
|
38
|
+
"README.md"
|
|
39
|
+
],
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "https://github.com/CreatelexLLC/createlexgenai"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://createlex.com"
|
|
45
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP Activity Tracker - Logs tool usage to the CreateLex backend
|
|
3
|
+
|
|
4
|
+
This module tracks MCP tool calls and sends usage data to the backend
|
|
5
|
+
for analytics and monitoring. All logging is non-blocking to avoid
|
|
6
|
+
impacting tool execution performance.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import json
|
|
11
|
+
import time
|
|
12
|
+
import threading
|
|
13
|
+
import queue
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from typing import Optional, Dict, Any
|
|
16
|
+
import logging
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger('mcp_activity_tracker')
|
|
19
|
+
|
|
20
|
+
# Configuration
|
|
21
|
+
ACTIVITY_API_ENDPOINT = os.environ.get('CREATELEX_API_URL', 'https://api.createlex.com') + '/api/mcp/activity'
|
|
22
|
+
BATCH_SIZE = 10 # Send activities in batches
|
|
23
|
+
FLUSH_INTERVAL = 30 # Flush every 30 seconds even if batch isn't full
|
|
24
|
+
MAX_QUEUE_SIZE = 100 # Maximum pending activities before dropping
|
|
25
|
+
|
|
26
|
+
class MCPActivityTracker:
|
|
27
|
+
"""
|
|
28
|
+
Asynchronous activity tracker for MCP tool calls.
|
|
29
|
+
|
|
30
|
+
Collects tool usage data and sends it to the backend in batches
|
|
31
|
+
to minimize network overhead and avoid blocking tool execution.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self):
|
|
35
|
+
self._activity_queue = queue.Queue(maxsize=MAX_QUEUE_SIZE)
|
|
36
|
+
self._worker_thread = None
|
|
37
|
+
self._shutdown = False
|
|
38
|
+
self._session_id = self._generate_session_id()
|
|
39
|
+
self._session_start = datetime.utcnow().isoformat()
|
|
40
|
+
self._tool_call_count = 0
|
|
41
|
+
|
|
42
|
+
# User info from environment (set by VSCode extension)
|
|
43
|
+
self._user_id = os.environ.get('CREATELEX_USER_ID')
|
|
44
|
+
self._user_email = os.environ.get('CREATELEX_USER_EMAIL') or os.environ.get('USER_EMAIL')
|
|
45
|
+
self._device_id = os.environ.get('CREATELEX_DEVICE_ID')
|
|
46
|
+
self._auth_token = os.environ.get('CREATELEX_AUTH_TOKEN')
|
|
47
|
+
|
|
48
|
+
# Start background worker
|
|
49
|
+
self._start_worker()
|
|
50
|
+
|
|
51
|
+
logger.info(f"Activity tracker initialized - Session: {self._session_id[:8]}...")
|
|
52
|
+
|
|
53
|
+
def _generate_session_id(self) -> str:
|
|
54
|
+
"""Generate a unique session ID"""
|
|
55
|
+
import hashlib
|
|
56
|
+
import uuid
|
|
57
|
+
return hashlib.sha256(f"{uuid.uuid4()}-{time.time()}".encode()).hexdigest()[:32]
|
|
58
|
+
|
|
59
|
+
def _start_worker(self):
|
|
60
|
+
"""Start the background worker thread"""
|
|
61
|
+
if self._worker_thread is None or not self._worker_thread.is_alive():
|
|
62
|
+
self._worker_thread = threading.Thread(target=self._worker_loop, daemon=True)
|
|
63
|
+
self._worker_thread.start()
|
|
64
|
+
logger.info("Activity tracker worker started")
|
|
65
|
+
|
|
66
|
+
def _worker_loop(self):
|
|
67
|
+
"""Background worker that sends batched activities"""
|
|
68
|
+
batch = []
|
|
69
|
+
last_flush = time.time()
|
|
70
|
+
|
|
71
|
+
while not self._shutdown:
|
|
72
|
+
try:
|
|
73
|
+
# Try to get an activity from the queue (with timeout)
|
|
74
|
+
try:
|
|
75
|
+
activity = self._activity_queue.get(timeout=1.0)
|
|
76
|
+
batch.append(activity)
|
|
77
|
+
except queue.Empty:
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
# Check if we should flush the batch
|
|
81
|
+
should_flush = (
|
|
82
|
+
len(batch) >= BATCH_SIZE or
|
|
83
|
+
(len(batch) > 0 and time.time() - last_flush >= FLUSH_INTERVAL)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if should_flush:
|
|
87
|
+
self._send_batch(batch)
|
|
88
|
+
batch = []
|
|
89
|
+
last_flush = time.time()
|
|
90
|
+
|
|
91
|
+
except Exception as e:
|
|
92
|
+
logger.error(f"Activity worker error: {e}")
|
|
93
|
+
time.sleep(1) # Back off on error
|
|
94
|
+
|
|
95
|
+
# Final flush on shutdown
|
|
96
|
+
if batch:
|
|
97
|
+
self._send_batch(batch)
|
|
98
|
+
|
|
99
|
+
def _send_batch(self, batch: list):
|
|
100
|
+
"""Send a batch of activities to the backend"""
|
|
101
|
+
if not batch:
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
import requests
|
|
106
|
+
|
|
107
|
+
payload = {
|
|
108
|
+
'session_id': self._session_id,
|
|
109
|
+
'user_id': self._user_id,
|
|
110
|
+
'user_email': self._user_email,
|
|
111
|
+
'device_id': self._device_id,
|
|
112
|
+
'activities': batch,
|
|
113
|
+
'timestamp': datetime.utcnow().isoformat()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
headers = {
|
|
117
|
+
'Content-Type': 'application/json'
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# Add auth token if available
|
|
121
|
+
if self._auth_token:
|
|
122
|
+
headers['Authorization'] = f'Bearer {self._auth_token}'
|
|
123
|
+
|
|
124
|
+
response = requests.post(
|
|
125
|
+
ACTIVITY_API_ENDPOINT,
|
|
126
|
+
json=payload,
|
|
127
|
+
headers=headers,
|
|
128
|
+
timeout=5 # Short timeout to avoid blocking
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if response.status_code == 200:
|
|
132
|
+
logger.debug(f"Sent {len(batch)} activities to backend")
|
|
133
|
+
else:
|
|
134
|
+
logger.warning(f"Failed to send activities: {response.status_code}")
|
|
135
|
+
|
|
136
|
+
except requests.RequestException as e:
|
|
137
|
+
logger.warning(f"Network error sending activities: {e}")
|
|
138
|
+
except Exception as e:
|
|
139
|
+
logger.error(f"Error sending activities: {e}")
|
|
140
|
+
|
|
141
|
+
def track_tool_call(
|
|
142
|
+
self,
|
|
143
|
+
tool_name: str,
|
|
144
|
+
arguments: Dict[str, Any],
|
|
145
|
+
success: bool,
|
|
146
|
+
duration_ms: float,
|
|
147
|
+
error: Optional[str] = None,
|
|
148
|
+
result_summary: Optional[str] = None
|
|
149
|
+
):
|
|
150
|
+
"""
|
|
151
|
+
Track a tool call.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
tool_name: Name of the MCP tool called
|
|
155
|
+
arguments: Arguments passed to the tool (sanitized)
|
|
156
|
+
success: Whether the tool call succeeded
|
|
157
|
+
duration_ms: Execution time in milliseconds
|
|
158
|
+
error: Error message if failed
|
|
159
|
+
result_summary: Brief summary of the result (for analytics)
|
|
160
|
+
"""
|
|
161
|
+
self._tool_call_count += 1
|
|
162
|
+
|
|
163
|
+
activity = {
|
|
164
|
+
'type': 'mcp_tool_call',
|
|
165
|
+
'tool_name': tool_name,
|
|
166
|
+
'success': success,
|
|
167
|
+
'duration_ms': duration_ms,
|
|
168
|
+
'timestamp': datetime.utcnow().isoformat(),
|
|
169
|
+
'call_number': self._tool_call_count,
|
|
170
|
+
# Sanitize arguments - remove potentially sensitive data
|
|
171
|
+
'argument_keys': list(arguments.keys()) if arguments else [],
|
|
172
|
+
'argument_count': len(arguments) if arguments else 0
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if error:
|
|
176
|
+
activity['error'] = str(error)[:200] # Truncate long errors
|
|
177
|
+
|
|
178
|
+
if result_summary:
|
|
179
|
+
activity['result_summary'] = result_summary[:100]
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
self._activity_queue.put_nowait(activity)
|
|
183
|
+
except queue.Full:
|
|
184
|
+
logger.warning("Activity queue full, dropping oldest entry")
|
|
185
|
+
try:
|
|
186
|
+
self._activity_queue.get_nowait()
|
|
187
|
+
self._activity_queue.put_nowait(activity)
|
|
188
|
+
except:
|
|
189
|
+
pass
|
|
190
|
+
|
|
191
|
+
def track_session_event(self, event_type: str, details: Optional[Dict] = None):
|
|
192
|
+
"""
|
|
193
|
+
Track a session event (startup, shutdown, error, etc.)
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
event_type: Type of event ('session_start', 'session_end', 'error', etc.)
|
|
197
|
+
details: Additional event details
|
|
198
|
+
"""
|
|
199
|
+
activity = {
|
|
200
|
+
'type': 'session_event',
|
|
201
|
+
'event_type': event_type,
|
|
202
|
+
'timestamp': datetime.utcnow().isoformat(),
|
|
203
|
+
'details': details or {}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
self._activity_queue.put_nowait(activity)
|
|
208
|
+
except queue.Full:
|
|
209
|
+
pass
|
|
210
|
+
|
|
211
|
+
def get_session_stats(self) -> Dict:
|
|
212
|
+
"""Get current session statistics"""
|
|
213
|
+
return {
|
|
214
|
+
'session_id': self._session_id,
|
|
215
|
+
'session_start': self._session_start,
|
|
216
|
+
'tool_call_count': self._tool_call_count,
|
|
217
|
+
'user_id': self._user_id,
|
|
218
|
+
'device_id': self._device_id,
|
|
219
|
+
'pending_activities': self._activity_queue.qsize()
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
def shutdown(self):
|
|
223
|
+
"""Graceful shutdown - flush pending activities"""
|
|
224
|
+
logger.info("Activity tracker shutting down...")
|
|
225
|
+
self._shutdown = True
|
|
226
|
+
|
|
227
|
+
# Track session end
|
|
228
|
+
self.track_session_event('session_end', {
|
|
229
|
+
'total_tool_calls': self._tool_call_count
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
# Wait for worker to finish (with timeout)
|
|
233
|
+
if self._worker_thread and self._worker_thread.is_alive():
|
|
234
|
+
self._worker_thread.join(timeout=5)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
# Global tracker instance
|
|
238
|
+
_tracker: Optional[MCPActivityTracker] = None
|
|
239
|
+
|
|
240
|
+
def get_tracker() -> MCPActivityTracker:
|
|
241
|
+
"""Get or create the global activity tracker"""
|
|
242
|
+
global _tracker
|
|
243
|
+
if _tracker is None:
|
|
244
|
+
_tracker = MCPActivityTracker()
|
|
245
|
+
_tracker.track_session_event('session_start', {
|
|
246
|
+
'platform': os.environ.get('MCP_PLATFORM', 'unknown'),
|
|
247
|
+
'version': os.environ.get('MCP_VERSION', 'unknown')
|
|
248
|
+
})
|
|
249
|
+
return _tracker
|
|
250
|
+
|
|
251
|
+
def track_tool_call(
|
|
252
|
+
tool_name: str,
|
|
253
|
+
arguments: Dict[str, Any],
|
|
254
|
+
success: bool,
|
|
255
|
+
duration_ms: float,
|
|
256
|
+
error: Optional[str] = None,
|
|
257
|
+
result_summary: Optional[str] = None
|
|
258
|
+
):
|
|
259
|
+
"""Convenience function to track a tool call"""
|
|
260
|
+
try:
|
|
261
|
+
tracker = get_tracker()
|
|
262
|
+
tracker.track_tool_call(
|
|
263
|
+
tool_name=tool_name,
|
|
264
|
+
arguments=arguments,
|
|
265
|
+
success=success,
|
|
266
|
+
duration_ms=duration_ms,
|
|
267
|
+
error=error,
|
|
268
|
+
result_summary=result_summary
|
|
269
|
+
)
|
|
270
|
+
except Exception as e:
|
|
271
|
+
# Never let tracking errors affect main functionality
|
|
272
|
+
logger.error(f"Failed to track tool call: {e}")
|
|
273
|
+
|
|
274
|
+
def shutdown_tracker():
|
|
275
|
+
"""Shutdown the global tracker"""
|
|
276
|
+
global _tracker
|
|
277
|
+
if _tracker:
|
|
278
|
+
_tracker.shutdown()
|
|
279
|
+
_tracker = None
|
|
280
|
+
|