thrust-cli 1.0.11 → 1.0.12

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,226 @@
1
+ using UnityEngine;
2
+ using UnityEditor;
3
+ using System.Net;
4
+ using System.Threading;
5
+ using System.IO;
6
+ using System.Text;
7
+ using System.Collections.Generic;
8
+ using UnityEngine.SceneManagement;
9
+
10
+ [InitializeOnLoad]
11
+ public static class ThrustMCPBridge
12
+ {
13
+ private static HttpListener listener;
14
+ private static Thread listenerThread;
15
+
16
+ // Buffers
17
+ private static List<string> activityBuffer = new List<string>();
18
+ private static List<string> consoleBuffer = new List<string>();
19
+ private static readonly object bufferLock = new object();
20
+
21
+ // Main Thread Dispatcher variables
22
+ private static string pendingAction = null;
23
+ private static string pendingArgument = null;
24
+ private static string actionResult = null;
25
+ private static volatile bool isProcessingOnMainThread = false;
26
+
27
+ private const int PORT = 8081;
28
+
29
+ static ThrustMCPBridge()
30
+ {
31
+ // 1. Hook into Unity Editor Events
32
+ EditorApplication.hierarchyChanged += OnHierarchyChanged;
33
+ EditorApplication.playModeStateChanged += OnPlayModeChanged;
34
+ UnityEditor.SceneManagement.EditorSceneManager.sceneSaved += OnSceneSaved;
35
+ Application.logMessageReceived += OnLogMessage;
36
+
37
+ // Hook into the main thread loop to process read requests
38
+ EditorApplication.update += OnEditorUpdate;
39
+
40
+ // 2. Start the local MCP Server
41
+ StartServer();
42
+ EditorApplication.quitting += StopServer;
43
+ }
44
+
45
+ private static void AddActivity(string activity)
46
+ {
47
+ lock (bufferLock)
48
+ {
49
+ activityBuffer.Add($"[{System.DateTime.Now.ToShortTimeString()}] {activity}");
50
+ if (activityBuffer.Count > 50) activityBuffer.RemoveAt(0);
51
+ }
52
+ }
53
+
54
+ private static void OnLogMessage(string logString, string stackTrace, LogType type)
55
+ {
56
+ lock (bufferLock)
57
+ {
58
+ if (type == LogType.Error || type == LogType.Exception || type == LogType.Warning)
59
+ {
60
+ consoleBuffer.Add($"[{type}] {logString}");
61
+ if (consoleBuffer.Count > 30) consoleBuffer.RemoveAt(0);
62
+ }
63
+ }
64
+ }
65
+
66
+ private static void OnHierarchyChanged() => AddActivity("Hierarchy changed.");
67
+ private static void OnSceneSaved(Scene scene) => AddActivity($"Saved scene: {scene.name}");
68
+ private static void OnPlayModeChanged(PlayModeStateChange state) => AddActivity($"Play mode state: {state}");
69
+
70
+ // --- HTTP SERVER LOOP (Background Thread) ---
71
+ private static void StartServer()
72
+ {
73
+ if (listener != null && listener.IsListening) return;
74
+ listener = new HttpListener();
75
+ listener.Prefixes.Add($"http://localhost:{PORT}/");
76
+ listener.Start();
77
+
78
+ listenerThread = new Thread(ListenForRequests) { IsBackground = true };
79
+ listenerThread.Start();
80
+ Debug.Log($"[Thrust MCP] Two-Way Unity Bridge active on port {PORT}...");
81
+ }
82
+
83
+ private static void ListenForRequests()
84
+ {
85
+ while (listener != null && listener.IsListening)
86
+ {
87
+ try { ProcessRequest(listener.GetContext()); }
88
+ catch (HttpListenerException) { /* Ignored on shutdown */ }
89
+ }
90
+ }
91
+
92
+ private static void ProcessRequest(HttpListenerContext context)
93
+ {
94
+ var request = context.Request;
95
+ var response = context.Response;
96
+
97
+ response.AddHeader("Access-Control-Allow-Origin", "*");
98
+ response.AddHeader("Access-Control-Allow-Headers", "Content-Type");
99
+
100
+ if (request.HttpMethod == "OPTIONS") { SendResponse(response, ""); return; }
101
+
102
+ string requestBody;
103
+ using (var reader = new StreamReader(request.InputStream, request.ContentEncoding))
104
+ {
105
+ requestBody = reader.ReadToEnd();
106
+ }
107
+
108
+ string resultText = "Unknown command";
109
+
110
+ // 1. Parse requested tool name (Crude JSON parsing to avoid dependencies)
111
+ if (requestBody.Contains("\"name\":\"get_recent_activity\"") || requestBody.Contains("\"name\": \"get_recent_activity\""))
112
+ {
113
+ lock (bufferLock)
114
+ {
115
+ resultText = activityBuffer.Count == 0 ? "No new activity" : string.Join("\\n", activityBuffer);
116
+ activityBuffer.Clear();
117
+ }
118
+ }
119
+ else if (requestBody.Contains("\"name\":\"get_console_logs\"") || requestBody.Contains("\"name\": \"get_console_logs\""))
120
+ {
121
+ lock (bufferLock)
122
+ {
123
+ resultText = consoleBuffer.Count == 0 ? "No recent errors/warnings." : string.Join("\\n", consoleBuffer).Replace("\"", "'");
124
+ consoleBuffer.Clear();
125
+ }
126
+ }
127
+ else if (requestBody.Contains("\"name\":\"get_hierarchy\"") || requestBody.Contains("\"name\": \"get_hierarchy\""))
128
+ {
129
+ resultText = DispatchToMainThread("get_hierarchy", "");
130
+ }
131
+ else if (requestBody.Contains("\"name\":\"get_properties\"") || requestBody.Contains("\"name\": \"get_properties\""))
132
+ {
133
+ // Extract the target parameter safely (e.g., "target": "Player")
134
+ string target = ExtractJsonValue(requestBody, "target");
135
+ resultText = DispatchToMainThread("get_properties", target);
136
+ }
137
+
138
+ // Format as JSON-RPC response
139
+ string responseJson = $@"{{
140
+ ""jsonrpc"": ""2.0"",
141
+ ""result"": {{ ""content"": [{{ ""type"": ""text"", ""text"": ""{resultText}"" }}] }}
142
+ }}";
143
+
144
+ SendResponse(response, responseJson);
145
+ }
146
+
147
+ // --- MAIN THREAD DISPATCHER ---
148
+ // Passes the request to the main thread and waits for the result
149
+ private static string DispatchToMainThread(string action, string arg)
150
+ {
151
+ pendingAction = action;
152
+ pendingArgument = arg;
153
+ actionResult = null;
154
+ isProcessingOnMainThread = true;
155
+
156
+ // Block background thread until main thread finishes
157
+ while (isProcessingOnMainThread) { Thread.Sleep(10); }
158
+
159
+ return actionResult;
160
+ }
161
+
162
+ // Executes on Unity's Main Thread
163
+ private static void OnEditorUpdate()
164
+ {
165
+ if (!isProcessingOnMainThread) return;
166
+
167
+ try
168
+ {
169
+ if (pendingAction == "get_hierarchy")
170
+ {
171
+ var rootObjects = SceneManager.GetActiveScene().GetRootGameObjects();
172
+ string hierarchy = "Active Scene Hierarchy:\\n";
173
+ foreach (var go in rootObjects) hierarchy += $"- {go.name}\\n";
174
+ actionResult = hierarchy;
175
+ }
176
+ else if (pendingAction == "get_properties")
177
+ {
178
+ if (string.IsNullOrEmpty(pendingArgument)) {
179
+ actionResult = "Error: No target specified.";
180
+ } else {
181
+ GameObject go = GameObject.Find(pendingArgument);
182
+ if (go == null) {
183
+ actionResult = $"GameObject '{pendingArgument}' not found in active scene.";
184
+ } else {
185
+ actionResult = $"Properties for '{go.name}':\\nPosition: {go.transform.position}\\nRotation: {go.transform.eulerAngles}\\nComponents:\\n";
186
+ foreach (var comp in go.GetComponents<Component>())
187
+ {
188
+ if(comp != null) actionResult += $"- {comp.GetType().Name}\\n";
189
+ }
190
+ }
191
+ }
192
+ }
193
+ }
194
+ catch (System.Exception e) { actionResult = $"Error reading Unity state: {e.Message}"; }
195
+
196
+ isProcessingOnMainThread = false; // Release the background thread
197
+ }
198
+
199
+ // Utility
200
+ private static string ExtractJsonValue(string json, string key)
201
+ {
202
+ string search = $"\"{key}\":";
203
+ int index = json.IndexOf(search);
204
+ if (index == -1) search = $"\"{key}\" :"; index = json.IndexOf(search);
205
+ if (index == -1) return "";
206
+
207
+ int start = json.IndexOf("\"", index + search.Length) + 1;
208
+ int end = json.IndexOf("\"", start);
209
+ return json.Substring(start, end - start);
210
+ }
211
+
212
+ private static void SendResponse(HttpListenerResponse response, string json)
213
+ {
214
+ byte[] buffer = Encoding.UTF8.GetBytes(json);
215
+ response.ContentType = "application/json";
216
+ response.ContentLength64 = buffer.Length;
217
+ response.OutputStream.Write(buffer, 0, buffer.Length);
218
+ response.OutputStream.Close();
219
+ }
220
+
221
+ private static void StopServer()
222
+ {
223
+ if (listener != null) { listener.Stop(); listener.Close(); listener = null; }
224
+ if (listenerThread != null) { listenerThread.Abort(); listenerThread = null; }
225
+ }
226
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thrust-cli",
3
- "version": "1.0.11",
3
+ "version": "1.0.12",
4
4
  "description": "The local agent for Thrust AI Director",
5
5
  "type": "module",
6
6
  "homepage": "https://thrust.web.app",
package/utils/daemon.js CHANGED
@@ -166,6 +166,56 @@ export async function startDaemon(preferredPort) {
166
166
  res.json({ success: true });
167
167
  });
168
168
 
169
+ // ==========================================
170
+ // EXTERNAL MCP DIRECT QUERY (AI OR MANUAL)
171
+ // ==========================================
172
+ app.post('/api/mcp/query', async (req, res) => {
173
+
174
+ const { serverName, toolName, targetArg } = req.body;
175
+ const config = getConfig();
176
+
177
+ // Find the requested server by name (e.g., "Unity")
178
+ const server = config.mcpServers?.find(s => s.name.toLowerCase() === serverName.toLowerCase());
179
+
180
+ if (!server) return res.status(404).json({ error: `MCP Server '${serverName}' not found.` });
181
+
182
+ try {
183
+ // Build the JSON-RPC Payload
184
+ const payload = {
185
+ jsonrpc: "2.0",
186
+ method: "tools/call",
187
+ params: {
188
+ name: toolName,
189
+ arguments: targetArg ? { target: targetArg } : {}
190
+ },
191
+ id: Date.now()
192
+ };
193
+
194
+ const response = await fetch(server.url, {
195
+ method: 'POST',
196
+ headers: { 'Content-Type': 'application/json' },
197
+ body: JSON.stringify(payload)
198
+ });
199
+
200
+ if (!response.ok) throw new Error("Server responded with error");
201
+
202
+ const data = await response.json();
203
+ const resultText = data.result?.content?.[0]?.text || "No data returned.";
204
+
205
+ // Log it locally so the user sees the system thinking
206
+ broadcastLocalLog('mcp', `⚡ Queried ${serverName} for ${toolName}.`);
207
+
208
+ // Inject this data directly into the activity buffer so the AI sees it on the next sync
209
+ fileActivityBuffer += `\n[${new Date().toLocaleTimeString()}] [MCP DIRECT QUERY RESULT] Source: ${serverName} | Tool: ${toolName} | Target: ${targetArg || 'none'} \nResult:\n${resultText}\n`;
210
+
211
+ res.json({ success: true, data: resultText });
212
+
213
+ } catch (e) {
214
+ broadcastLocalLog('error', `⚠️ Failed to query MCP server: ${serverName}`);
215
+ res.status(500).json({ error: e.message });
216
+ }
217
+ });
218
+
169
219
  // ==========================================
170
220
  // CORE DASHBOARD API
171
221
  // ==========================================