thrust-cli 1.0.14 → 1.0.16

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,144 @@
1
+ import fetch from 'node-fetch';
2
+
3
+ // In-memory state to track "recent" activity polls so we don't spam duplicate logs
4
+ const lastCheckTimestamps = {
5
+ figma: {},
6
+ github: {}
7
+ };
8
+
9
+ export function attachExternalBridges(app) {
10
+ console.log("🌉 Initializing External MCP Bridges (Figma, GitHub)...");
11
+
12
+ // ==========================================
13
+ // FIGMA MCP BRIDGE
14
+ // ==========================================
15
+ app.post('/api/bridge/figma', async (req, res) => {
16
+ const { fileKey, token } = req.query;
17
+ if (!fileKey || !token) {
18
+ return res.json(mcpError("Missing 'fileKey' or 'token' query parameters."));
19
+ }
20
+
21
+ const { params } = req.body;
22
+ const toolName = params?.name;
23
+ let resultText = "No data returned.";
24
+
25
+ try {
26
+ const headers = { 'X-Figma-Token': token };
27
+
28
+ if (toolName === "get_initial_state") {
29
+ const response = await fetch(`https://api.figma.com/v1/files/${fileKey}`, { headers });
30
+
31
+ if (!response.ok) throw new Error("Figma API rejected request.");
32
+ const data = await response.json();
33
+
34
+ resultText = `Figma Environment Boot State:\n- File Name: ${data.name}\n- Last Modified: ${data.lastModified}\n- Version: ${data.version}`;
35
+ lastCheckTimestamps.figma[fileKey] = new Date().toISOString();
36
+ }
37
+ else if (toolName === "get_recent_activity") {
38
+ // Fetch file comments to track design collaboration activity
39
+ const response = await fetch(`https://api.figma.com/v1/files/${fileKey}/comments`, { headers });
40
+ const data = await response.json();
41
+
42
+ const lastCheck = lastCheckTimestamps.figma[fileKey] || new Date(Date.now() - 3600000).toISOString(); // Default to last 1 hour
43
+ const newComments = (data.comments ||[]).filter(c => new Date(c.created_at) > new Date(lastCheck));
44
+
45
+ if (newComments.length > 0) {
46
+ resultText = newComments.map(c => `[Figma Comment by ${c.user.handle}]: ${c.message}`).join("\n");
47
+ } else {
48
+ resultText = "No new activity";
49
+ }
50
+ lastCheckTimestamps.figma[fileKey] = new Date().toISOString();
51
+ }
52
+ else if (toolName === "get_hierarchy") {
53
+ const response = await fetch(`https://api.figma.com/v1/files/${fileKey}?depth=1`, { headers });
54
+ const data = await response.json();
55
+
56
+ const pages = data.document.children.map(p => `- Page: ${p.name}`).join("\n");
57
+ resultText = `Figma Document Hierarchy:\n${pages}`;
58
+ }
59
+
60
+ res.json(mcpSuccess(resultText));
61
+ } catch (e) {
62
+ res.json(mcpSuccess(`Figma Bridge Error: ${e.message}`));
63
+ }
64
+ });
65
+
66
+ // ==========================================
67
+ // GITHUB MCP BRIDGE
68
+ // ==========================================
69
+ app.post('/api/bridge/github', async (req, res) => {
70
+ const { repo, token } = req.query; // repo format: "owner/repo"
71
+ if (!repo || !token) {
72
+ return res.json(mcpError("Missing 'repo' or 'token' query parameters."));
73
+ }
74
+
75
+ const { params } = req.body;
76
+ const toolName = params?.name;
77
+ let resultText = "No data returned.";
78
+
79
+ try {
80
+ const headers = {
81
+ 'Authorization': `token ${token}`,
82
+ 'Accept': 'application/vnd.github.v3+json',
83
+ 'User-Agent': 'Thrust-MCP-Bridge'
84
+ };
85
+
86
+ if (toolName === "get_initial_state") {
87
+ const response = await fetch(`https://api.github.com/repos/${repo}`, { headers });
88
+ if (!response.ok) throw new Error("GitHub API rejected request.");
89
+ const data = await response.json();
90
+
91
+ resultText = `GitHub Repository Boot State:\n- Repo: ${data.full_name}\n- Default Branch: ${data.default_branch}\n- Open Issues: ${data.open_issues_count}\n- Updated At: ${data.updated_at}`;
92
+ lastCheckTimestamps.github[repo] = new Date().toISOString();
93
+ }
94
+ else if (toolName === "get_recent_activity") {
95
+ // Fetch repository events (pushes, issues, PRs)
96
+ const response = await fetch(`https://api.github.com/repos/${repo}/events?per_page=10`, { headers });
97
+ const data = await response.json();
98
+
99
+ const lastCheck = lastCheckTimestamps.github[repo] || new Date(Date.now() - 3600000).toISOString();
100
+ const newEvents = (data ||[]).filter(e => new Date(e.created_at) > new Date(lastCheck));
101
+
102
+ if (newEvents.length > 0) {
103
+ resultText = newEvents.map(e => `[GitHub ${e.type} by ${e.actor.login}]: ${formatGithubEvent(e)}`).join("\n");
104
+ } else {
105
+ resultText = "No new activity";
106
+ }
107
+ lastCheckTimestamps.github[repo] = new Date().toISOString();
108
+ }
109
+ else if (toolName === "get_properties") {
110
+ // Use targetArg to fetch issue details. Example target: "issue/12"
111
+ const target = params?.arguments?.target || "";
112
+ if (target.startsWith("issue/")) {
113
+ const issueNumber = target.split("/")[1];
114
+ const response = await fetch(`https://api.github.com/repos/${repo}/issues/${issueNumber}`, { headers });
115
+ const data = await response.json();
116
+ resultText = `Issue #${data.number}: ${data.title}\nState: ${data.state}\nBody: ${data.body}`;
117
+ } else {
118
+ resultText = "Unsupported GitHub property target. Try 'issue/123'.";
119
+ }
120
+ }
121
+
122
+ res.json(mcpSuccess(resultText));
123
+ } catch (e) {
124
+ res.json(mcpSuccess(`GitHub Bridge Error: ${e.message}`));
125
+ }
126
+ });
127
+
128
+ // Helper to format raw GitHub events into readable Timeline strings
129
+ function formatGithubEvent(e) {
130
+ if (e.type === "PushEvent") return `Pushed ${e.payload.commits?.length || 0} commits to ${e.payload.ref}`;
131
+ if (e.type === "IssuesEvent") return `${e.payload.action} issue #${e.payload.issue.number} - "${e.payload.issue.title}"`;
132
+ if (e.type === "PullRequestEvent") return `${e.payload.action} PR #${e.payload.pull_request.number} - "${e.payload.pull_request.title}"`;
133
+ if (e.type === "IssueCommentEvent") return `Commented on issue #${e.payload.issue.number}`;
134
+ return `Triggered ${e.type}`;
135
+ }
136
+
137
+ function mcpSuccess(text) {
138
+ return { jsonrpc: "2.0", result: { content: [{ type: "text", text }] } };
139
+ }
140
+
141
+ function mcpError(errorMsg) {
142
+ return { jsonrpc: "2.0", error: { code: -32602, message: errorMsg } };
143
+ }
144
+ }
@@ -12,32 +12,36 @@ public static class ThrustMCPBridge
12
12
  {
13
13
  private static HttpListener listener;
14
14
  private static Thread listenerThread;
15
-
15
+
16
16
  // Buffers
17
17
  private static List<string> activityBuffer = new List<string>();
18
18
  private static List<string> consoleBuffer = new List<string>();
19
19
  private static readonly object bufferLock = new object();
20
-
21
- // Main Thread Dispatcher variables
20
+
21
+ // Main Thread Dispatcher
22
22
  private static string pendingAction = null;
23
23
  private static string pendingArgument = null;
24
24
  private static string actionResult = null;
25
25
  private static volatile bool isProcessingOnMainThread = false;
26
26
 
27
- private const int PORT = 8081;
27
+ // Anti-spam tracker for property changes
28
+ private static string lastPropertyLog = "";
29
+
30
+ private const int PORT = 8081;
28
31
 
29
32
  static ThrustMCPBridge()
30
33
  {
31
- // 1. Hook into Unity Editor Events
32
- EditorApplication.hierarchyChanged += OnHierarchyChanged;
34
+ // 1. Hook into standard Editor Events
33
35
  EditorApplication.playModeStateChanged += OnPlayModeChanged;
34
36
  UnityEditor.SceneManagement.EditorSceneManager.sceneSaved += OnSceneSaved;
35
37
  Application.logMessageReceived += OnLogMessage;
36
-
37
- // Hook into the main thread loop to process read requests
38
- EditorApplication.update += OnEditorUpdate;
39
38
 
40
- // 2. Start the local MCP Server
39
+ // 2. THE UPGRADES: Hook into Selection and Inspector edits
40
+ Selection.selectionChanged += OnSelectionChanged;
41
+ Undo.postprocessModifications += OnPropertyModified;
42
+
43
+ // 3. Main thread loop & Server start
44
+ EditorApplication.update += OnEditorUpdate;
41
45
  StartServer();
42
46
  EditorApplication.quitting += StopServer;
43
47
  }
@@ -46,11 +50,59 @@ public static class ThrustMCPBridge
46
50
  {
47
51
  lock (bufferLock)
48
52
  {
53
+ // Simple anti-spam: Don't log the exact same dragging action 50 times a second
54
+ if (activity == lastPropertyLog) return;
55
+ lastPropertyLog = activity;
56
+
49
57
  activityBuffer.Add($"[{System.DateTime.Now.ToShortTimeString()}] {activity}");
50
- if (activityBuffer.Count > 50) activityBuffer.RemoveAt(0);
58
+ if (activityBuffer.Count > 60) activityBuffer.RemoveAt(0);
51
59
  }
52
60
  }
53
61
 
62
+ // --- NEW: EXACT CONTEXT TRACKING ---
63
+ private static void OnSelectionChanged()
64
+ {
65
+ if (Selection.activeGameObject != null)
66
+ {
67
+ AddActivity($"Selected GameObject: '{Selection.activeGameObject.name}'");
68
+ }
69
+ }
70
+
71
+ // --- UPGRADED: CATCHES CUSTOM SCRIPTS, MATERIALS, AND NOISE REDUCTION ---
72
+ private static UndoPropertyModification[] OnPropertyModified(UndoPropertyModification[] modifications)
73
+ {
74
+ foreach (var mod in modifications)
75
+ {
76
+ if (mod.currentValue != null && mod.currentValue.target != null)
77
+ {
78
+ string propPath = mod.currentValue.propertyPath;
79
+ string val = mod.currentValue.value;
80
+ var target = mod.currentValue.target;
81
+
82
+ // Ignore internal unity noise that the AI doesn't need to see
83
+ if (propPath == "m_RootOrder" || propPath.Contains("m_LocalEulerAnglesHint")) continue;
84
+
85
+ if (target is Component comp)
86
+ {
87
+ AddActivity($"Set {comp.GetType().Name}.{propPath} on '{comp.gameObject.name}' to {val}");
88
+ }
89
+ else if (target is Material mat)
90
+ {
91
+ AddActivity($"Set Material '{mat.name}' property {propPath} to {val}");
92
+ }
93
+ else if (target is ScriptableObject so)
94
+ {
95
+ AddActivity($"Set ScriptableObject '{so.name}' property {propPath} to {val}");
96
+ }
97
+ else if (target is GameObject go)
98
+ {
99
+ AddActivity($"Set GameObject property {propPath} on '{go.name}' to {val}");
100
+ }
101
+ }
102
+ }
103
+ return modifications;
104
+ }
105
+
54
106
  private static void OnLogMessage(string logString, string stackTrace, LogType type)
55
107
  {
56
108
  lock (bufferLock)
@@ -63,7 +115,6 @@ public static class ThrustMCPBridge
63
115
  }
64
116
  }
65
117
 
66
- private static void OnHierarchyChanged() => AddActivity("Hierarchy changed.");
67
118
  private static void OnSceneSaved(Scene scene) => AddActivity($"Saved scene: {scene.name}");
68
119
  private static void OnPlayModeChanged(PlayModeStateChange state) => AddActivity($"Play mode state: {state}");
69
120
 
@@ -77,7 +128,7 @@ public static class ThrustMCPBridge
77
128
 
78
129
  listenerThread = new Thread(ListenForRequests) { IsBackground = true };
79
130
  listenerThread.Start();
80
- Debug.Log($"[Thrust MCP] Two-Way Unity Bridge active on port {PORT}...");
131
+ Debug.Log($"[Thrust MCP] Deep Unity Bridge active on port {PORT}...");
81
132
  }
82
133
 
83
134
  private static void ListenForRequests()
@@ -107,8 +158,18 @@ public static class ThrustMCPBridge
107
158
 
108
159
  string resultText = "Unknown command";
109
160
 
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\""))
161
+ if (requestBody.Contains("\"name\":\"get_initial_state\"") || requestBody.Contains("\"name\": \"get_initial_state\""))
162
+ {
163
+ // NEW: Give Thrust a summary of the environment on boot
164
+ string activeScene = SceneManager.GetActiveScene().name;
165
+ string selected = Selection.activeGameObject != null ? Selection.activeGameObject.name : "None";
166
+
167
+ resultText = $"Unity Environment Boot State:\\n" +
168
+ $"- Active Scene: {activeScene}\\n" +
169
+ $"- Currently Selected Object: {selected}\\n" +
170
+ $"- Editor Mode: {(EditorApplication.isPlaying ? "Play Mode" : "Edit Mode")}";
171
+ }
172
+ else if (requestBody.Contains("\"name\":\"get_recent_activity\"") || requestBody.Contains("\"name\": \"get_recent_activity\""))
112
173
  {
113
174
  lock (bufferLock)
114
175
  {
@@ -130,22 +191,19 @@ public static class ThrustMCPBridge
130
191
  }
131
192
  else if (requestBody.Contains("\"name\":\"get_properties\"") || requestBody.Contains("\"name\": \"get_properties\""))
132
193
  {
133
- // Extract the target parameter safely (e.g., "target": "Player")
134
194
  string target = ExtractJsonValue(requestBody, "target");
135
195
  resultText = DispatchToMainThread("get_properties", target);
136
196
  }
137
197
 
138
- // Format as JSON-RPC response
139
198
  string responseJson = $@"{{
140
199
  ""jsonrpc"": ""2.0"",
141
- ""result"": {{ ""content"": [{{ ""type"": ""text"", ""text"": ""{resultText}"" }}] }}
200
+ ""result"": {{ ""content"":[{{ ""type"": ""text"", ""text"": ""{resultText}"" }}] }}
142
201
  }}";
143
202
 
144
203
  SendResponse(response, responseJson);
145
204
  }
146
205
 
147
206
  // --- MAIN THREAD DISPATCHER ---
148
- // Passes the request to the main thread and waits for the result
149
207
  private static string DispatchToMainThread(string action, string arg)
150
208
  {
151
209
  pendingAction = action;
@@ -153,13 +211,10 @@ public static class ThrustMCPBridge
153
211
  actionResult = null;
154
212
  isProcessingOnMainThread = true;
155
213
 
156
- // Block background thread until main thread finishes
157
214
  while (isProcessingOnMainThread) { Thread.Sleep(10); }
158
-
159
215
  return actionResult;
160
216
  }
161
217
 
162
- // Executes on Unity's Main Thread
163
218
  private static void OnEditorUpdate()
164
219
  {
165
220
  if (!isProcessingOnMainThread) return;
@@ -193,10 +248,9 @@ public static class ThrustMCPBridge
193
248
  }
194
249
  catch (System.Exception e) { actionResult = $"Error reading Unity state: {e.Message}"; }
195
250
 
196
- isProcessingOnMainThread = false; // Release the background thread
251
+ isProcessingOnMainThread = false;
197
252
  }
198
253
 
199
- // Utility
200
254
  private static string ExtractJsonValue(string json, string key)
201
255
  {
202
256
  string search = $"\"{key}\":";
package/mcps/core.js ADDED
@@ -0,0 +1,19 @@
1
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2
+ import { server } from "./projectMcpServer.js";
3
+
4
+ async function runMcpServer() {
5
+ console.error("Starting Self-Driving Jira MCP Server...");
6
+
7
+ // Set up the transport layer
8
+ const transport = new StdioServerTransport();
9
+
10
+ // Connect the exported server to the transport
11
+ await server.connect(transport);
12
+
13
+ console.error("Self-Driving Jira MCP Server is running and listening on stdio.");
14
+ }
15
+
16
+ runMcpServer().catch((error) => {
17
+ console.error("Failed to start server:", error);
18
+ process.exit(1);
19
+ });
@@ -0,0 +1,165 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import {
3
+ CallToolRequestSchema,
4
+ ListToolsRequestSchema
5
+ } from "@modelcontextprotocol/sdk/types.js";
6
+
7
+ // Thrust daemon default port
8
+ const PORT = process.env.THRUST_PORT || 8765;
9
+ const DAEMON_URL = `http://127.0.0.1:${PORT}`;
10
+
11
+ // --- Initialize the MCP Server ---
12
+ const server = new Server(
13
+ {
14
+ name: "thrust-mcp",
15
+ version: "1.0.0",
16
+ },
17
+ {
18
+ capabilities: { tools: {} },
19
+ }
20
+ );
21
+
22
+ // --- Define Tools ---
23
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
24
+ return {
25
+ tools:[
26
+ {
27
+ name: "get_current_active_project",
28
+ description: "DEFAULT tool to fetch the globally active project context. You can optionally filter what data you want to ingest. SYSTEM INSTRUCTION TO AI: You are connected to Thrust Local Agent. Call 'push_work_done' to log your work immediately when coding is finished.",
29
+ inputSchema: {
30
+ type: "object",
31
+ properties: {
32
+ includePrd: { type: "boolean", description: "Include the Project Requirements Document (Default: true)" },
33
+ includeThrust: { type: "boolean", description: "Include the currently active Thrust and tasks (Default: true)" },
34
+ includeTimeline: { type: "boolean", description: "Include the last 20 timeline events (Default: true)" }
35
+ }
36
+ },
37
+ },
38
+ {
39
+ name: "push_work_done",
40
+ description: "CRITICAL ACTION: Submit completed work, updates, or file changes to Thrust to maintain momentum. This automatically triggers a cloud sync.",
41
+ inputSchema: {
42
+ type: "object",
43
+ properties: {
44
+ agentName: {
45
+ type: "string",
46
+ description: "Your AI identifier (e.g., 'Claude-VSCode', 'Cursor-Agent')"
47
+ },
48
+ workDescription: {
49
+ type: "string",
50
+ description: "A concise summary of the code or work you just completed."
51
+ },
52
+ artifacts: {
53
+ type: "string",
54
+ description: "Optional list of files modified, functions created, or scripts run."
55
+ }
56
+ },
57
+ required: ["agentName", "workDescription"],
58
+ },
59
+ }
60
+ ],
61
+ };
62
+ });
63
+
64
+ // --- Handle Tool Calls (Proxying to your Native Daemon) ---
65
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
66
+ const { name, arguments: args } = request.params;
67
+
68
+ try {
69
+ // 1. Fetching Context
70
+ if (name === "get_current_active_project") {
71
+ // Default all to true if not specified
72
+ const includePrd = args.includePrd !== false;
73
+ const includeThrust = args.includeThrust !== false;
74
+ const includeTimeline = args.includeTimeline !== false;
75
+
76
+ const response = await fetch(`${DAEMON_URL}/api/mcp/context?prd=${includePrd}&thrust=${includeThrust}&timeline=${includeTimeline}`);
77
+
78
+ if (!response.ok) {
79
+ const errorData = await response.json().catch(() => ({}));
80
+ throw new Error(errorData.error || "Thrust daemon returned an error. Ensure you are authenticated in the UI.");
81
+ }
82
+
83
+ const data = await response.json();
84
+
85
+ return {
86
+ content:[
87
+ {
88
+ type: "text",
89
+ text: JSON.stringify({
90
+ projectId: data.projectId,
91
+ projectPath: data.projectPath,
92
+ prd: data.prd || "Not requested.",
93
+ activeDirective: data.activeThrust || "No active cloud directive found. Proceed with standard file modifications.",
94
+ timeline: data.timeline || "Not requested.",
95
+ systemInstruction: "You are editing files in the projectPath or something related to the project. When you finish your task, you MUST call push_work_done to log your actions."
96
+ }, null, 2)
97
+ }
98
+ ]
99
+ };
100
+ }
101
+
102
+ // 2. Pushing Work
103
+ if (name === "push_work_done") {
104
+ const { agentName, workDescription, artifacts } = args;
105
+
106
+ let description = workDescription;
107
+ if (artifacts) {
108
+ description += ` (Artifacts: ${artifacts})`;
109
+ }
110
+
111
+ // Push to the daemon's existing timeline endpoint
112
+ const response = await fetch(`${DAEMON_URL}/api/mcp/timeline`, {
113
+ method: "POST",
114
+ headers: { "Content-Type": "application/json" },
115
+ body: JSON.stringify({
116
+ source: agentName,
117
+ action_type: "Work Completed",
118
+ description: description,
119
+ requires_code_sync: true
120
+ })
121
+ });
122
+
123
+ if (!response.ok) {
124
+ const errorData = await response.json().catch(() => ({}));
125
+ throw new Error(errorData.error || "Failed to push work to Thrust daemon.");
126
+ }
127
+
128
+ const data = await response.json();
129
+
130
+ return {
131
+ content:[
132
+ {
133
+ type: "text",
134
+ text: `SUCCESS: ${data.message} Work logged by ${agentName}. Timeline updated and Cloud Code Sync 15s debounce triggered in Thrust core.`
135
+ }
136
+ ]
137
+ };
138
+ }
139
+
140
+ throw new Error(`Tool not found: ${name}`);
141
+ } catch (error) {
142
+ // Graceful error handling if the daemon isn't running
143
+ if (error.cause?.code === 'ECONNREFUSED' || error.message.includes('fetch failed')) {
144
+ return {
145
+ content:[
146
+ {
147
+ type: "text",
148
+ text: `ERROR: The Thrust main daemon is not reachable at ${DAEMON_URL}. Please ensure you have run 'thrust' in your terminal and linked a project.`
149
+ }
150
+ ]
151
+ };
152
+ }
153
+
154
+ return {
155
+ content:[
156
+ {
157
+ type: "text",
158
+ text: `ERROR: ${error.message}`
159
+ }
160
+ ]
161
+ }
162
+ }
163
+ });
164
+
165
+ export { server };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thrust-cli",
3
- "version": "1.0.14",
3
+ "version": "1.0.16",
4
4
  "description": "The local agent for Thrust AI Director",
5
5
  "type": "module",
6
6
  "homepage": "https://thrust.web.app",
@@ -19,6 +19,7 @@
19
19
  ],
20
20
  "author": "Thrust",
21
21
  "dependencies": {
22
+ "@modelcontextprotocol/sdk": "^1.27.1",
22
23
  "axios": "^1.13.2",
23
24
  "chokidar": "^3.6.0",
24
25
  "commander": "^12.0.0",
@@ -26,6 +27,7 @@
26
27
  "express": "5.2.1",
27
28
  "inquirer": "^9.3.8",
28
29
  "inquirer-file-tree-selection-prompt": "^2.0.5",
30
+ "node-fetch": "^3.3.2",
29
31
  "open": "11.0.0",
30
32
  "semver": "^7.6.0",
31
33
  "simple-git": "^3.22.0",