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.
- package/mcps/ThrustMCPBridge.cs +226 -0
- package/package.json +1 -1
- package/utils/daemon.js +50 -0
|
@@ -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
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
|
// ==========================================
|