thrust-cli 1.0.14 → 1.0.15

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 CHANGED
@@ -1,3 +1,4 @@
1
1
  **tt-package-demo**
2
2
 
3
3
  A demo package for Total TypeScript.
4
+ # thrust-cli
package/index.js CHANGED
@@ -8,10 +8,14 @@ import { fileURLToPath } from 'url';
8
8
  import { startDaemon } from './utils/daemon.js';
9
9
  import { getConfig, saveConfig } from './utils/config.js';
10
10
 
11
+ // --- MCP Imports ---
12
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13
+ import { server } from "./mcps/projectMcpServer.js";
14
+
11
15
  const __filename = fileURLToPath(import.meta.url);
12
16
  const __dirname = path.dirname(__filename);
13
17
 
14
- let packageJson = { version: "1.0.0" };
18
+ let packageJson = { version: "1.0.15" };
15
19
  try {
16
20
  packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
17
21
  } catch (e) {
@@ -29,7 +33,7 @@ program
29
33
  console.log('🚀 Booting Thrust Agent...');
30
34
 
31
35
  await setupNativeStartup();
32
-
36
+
33
37
  startDaemon(parseInt(options.port, 10));
34
38
  });
35
39
 
@@ -52,10 +56,30 @@ program
52
56
  config.activeLeadId = leadId;
53
57
 
54
58
  saveConfig(config);
55
- console.log(`✅ Successfully linked Lead [${leadId}] to ${absolutePath}`);
59
+ console.log(`✅ Successfully linked Lead[${leadId}] to ${absolutePath}`);
56
60
  console.log(`Type 'thrust' to begin tracking.`);
57
61
  });
58
62
 
63
+ // --- NEW MCP COMMAND ---
64
+ program
65
+ .command('mcp')
66
+ .description('Start the Thrust MCP Server over Stdio for AI clients (Claude, VS Code, Cursor)')
67
+ .action(async () => {
68
+ try {
69
+ // IMPORTANT: We must use console.error here.
70
+ // console.log prints to stdout, which corrupts the JSON-RPC stream the AI expects.
71
+ console.error('Starting Thrust MCP Server over Stdio...');
72
+
73
+ const transport = new StdioServerTransport();
74
+ await server.connect(transport);
75
+
76
+ console.error('Thrust MCP Server is ready and listening.');
77
+ } catch (error) {
78
+ console.error('MCP Server crashed:', error);
79
+ process.exit(1);
80
+ }
81
+ });
82
+
59
83
  program.parse(process.argv);
60
84
 
61
85
  // --- NATIVE, CROSS-PLATFORM OS STARTUP LOGIC ---
@@ -71,7 +95,7 @@ async function setupNativeStartup() {
71
95
  try {
72
96
  const startupFolder = path.join(os.homedir(), 'AppData', 'Roaming', 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup');
73
97
  const batFilePath = path.join(startupFolder, 'thrust-agent.bat');
74
-
98
+
75
99
  // Clean batch script that simply calls the global `thrust` command
76
100
  const batContent = `@echo off\ntitle Thrust Local Agent\necho Starting Thrust...\ntimeout /t 2 /nobreak > NUL\nthrust\npause`;
77
101
 
@@ -81,14 +105,14 @@ async function setupNativeStartup() {
81
105
  } catch (err) {
82
106
  console.log(`⚠️ Failed to create Windows startup script: ${err.message}`);
83
107
  }
84
- }
85
-
108
+ }
109
+
86
110
  // 3. macOS - Requires absolute paths due to empty launchd $PATH
87
111
  else if (process.platform === 'darwin') {
88
112
  try {
89
113
  const launchAgentsDir = path.join(os.homedir(), 'Library', 'LaunchAgents');
90
114
  const plistPath = path.join(launchAgentsDir, 'com.thrust.agent.plist');
91
-
115
+
92
116
  const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
93
117
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
94
118
  <plist version="1.0">
@@ -109,7 +133,7 @@ async function setupNativeStartup() {
109
133
 
110
134
  if (!fs.existsSync(launchAgentsDir)) fs.mkdirSync(launchAgentsDir, { recursive: true });
111
135
  fs.writeFileSync(plistPath, plistContent, 'utf8');
112
-
136
+
113
137
  import('child_process').then(({ exec }) => {
114
138
  exec(`launchctl load -w "${plistPath}"`, () => {});
115
139
  });
@@ -117,8 +141,8 @@ async function setupNativeStartup() {
117
141
  } catch (err) {
118
142
  console.log(`⚠️ Failed to create macOS LaunchAgent: ${err.message}`);
119
143
  }
120
- }
121
-
144
+ }
145
+
122
146
  // 4. Linux - Requires absolute paths for FreeDesktop compliance
123
147
  else if (process.platform === 'linux') {
124
148
  try {
@@ -129,7 +153,7 @@ async function setupNativeStartup() {
129
153
 
130
154
  const autostartDir = path.join(os.homedir(), '.config', 'autostart');
131
155
  const desktopPath = path.join(autostartDir, 'thrust-agent.desktop');
132
-
156
+
133
157
  const desktopContent = `[Desktop Entry]
134
158
  Type=Application
135
159
  Exec="${process.execPath}" "${__filename}"
@@ -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,155 @@
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://localhost:${PORT}`;
10
+ const DAEMON_URL = `http://127.0.0.1:${PORT}`;
11
+
12
+ // --- Initialize the MCP Server ---
13
+ const server = new Server(
14
+ {
15
+ name: "thrust-mcp",
16
+ version: "1.0.0",
17
+ },
18
+ {
19
+ capabilities: { tools: {} },
20
+ }
21
+ );
22
+
23
+ // --- Define Tools ---
24
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
25
+ return {
26
+ tools:[
27
+ {
28
+ name: "get_current_active_project",
29
+ description: "DEFAULT tool to fetch the globally active project context. SYSTEM INSTRUCTION TO AI: You are connected to Thrust Local Agent. Call 'push_work_done' to log your work immediately when coding is finished.",
30
+ inputSchema: {
31
+ type: "object",
32
+ properties: {}
33
+ },
34
+ },
35
+ {
36
+ name: "push_work_done",
37
+ description: "CRITICAL ACTION: Submit completed work, updates, or file changes to Thrust to maintain momentum. This automatically triggers a cloud sync.",
38
+ inputSchema: {
39
+ type: "object",
40
+ properties: {
41
+ agentName: {
42
+ type: "string",
43
+ description: "Your AI identifier (e.g., 'Claude-VSCode', 'Cursor-Agent')"
44
+ },
45
+ workDescription: {
46
+ type: "string",
47
+ description: "A concise summary of the code or work you just completed."
48
+ },
49
+ artifacts: {
50
+ type: "string",
51
+ description: "Optional list of files modified, functions created, or scripts run."
52
+ }
53
+ },
54
+ required: ["agentName", "workDescription"],
55
+ },
56
+ }
57
+ ],
58
+ };
59
+ });
60
+
61
+ // --- Handle Tool Calls (Proxying to your Native Daemon) ---
62
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
63
+ const { name, arguments: args } = request.params;
64
+
65
+ try {
66
+ // 1. Fetching Context
67
+ if (name === "get_current_active_project") {
68
+ const response = await fetch(`${DAEMON_URL}/api/mcp/context`);
69
+
70
+ if (!response.ok) {
71
+ const errorData = await response.json().catch(() => ({}));
72
+ throw new Error(errorData.error || "Thrust daemon returned an error. Ensure you are authenticated in the UI.");
73
+ }
74
+
75
+ const data = await response.json();
76
+
77
+ return {
78
+ content:[
79
+ {
80
+ type: "text",
81
+ text: JSON.stringify({
82
+ projectId: data.projectId,
83
+ projectPath: data.projectPath,
84
+ activeDirective: data.activeThrust || "No active cloud directive found. Proceed with standard file modifications.",
85
+ 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."
86
+ }, null, 2)
87
+ }
88
+ ]
89
+ };
90
+ }
91
+
92
+ // 2. Pushing Work
93
+ if (name === "push_work_done") {
94
+ const { agentName, workDescription, artifacts } = args;
95
+
96
+ let description = workDescription;
97
+ if (artifacts) {
98
+ description += ` (Artifacts: ${artifacts})`;
99
+ }
100
+
101
+ // Push to the daemon's existing timeline endpoint
102
+ const response = await fetch(`${DAEMON_URL}/api/mcp/timeline`, {
103
+ method: "POST",
104
+ headers: { "Content-Type": "application/json" },
105
+ body: JSON.stringify({
106
+ source: agentName,
107
+ action_type: "Work Completed",
108
+ description: description,
109
+ requires_code_sync: true
110
+ })
111
+ });
112
+
113
+ if (!response.ok) {
114
+ const errorData = await response.json().catch(() => ({}));
115
+ throw new Error(errorData.error || "Failed to push work to Thrust daemon.");
116
+ }
117
+
118
+ const data = await response.json();
119
+
120
+ return {
121
+ content:[
122
+ {
123
+ type: "text",
124
+ text: `SUCCESS: ${data.message} Work logged by ${agentName}. Timeline updated and Cloud Code Sync 15s debounce triggered in Thrust core.`
125
+ }
126
+ ]
127
+ };
128
+ }
129
+
130
+ throw new Error(`Tool not found: ${name}`);
131
+ } catch (error) {
132
+ // Graceful error handling if the daemon isn't running
133
+ if (error.cause?.code === 'ECONNREFUSED' || error.message.includes('fetch failed')) {
134
+ return {
135
+ content:[
136
+ {
137
+ type: "text",
138
+ 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.`
139
+ }
140
+ ]
141
+ };
142
+ }
143
+
144
+ return {
145
+ content:[
146
+ {
147
+ type: "text",
148
+ text: `ERROR: ${error.message}`
149
+ }
150
+ ]
151
+ }
152
+ }
153
+ });
154
+
155
+ 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.15",
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",
package/utils/daemon.js CHANGED
@@ -11,6 +11,7 @@ import cors from 'cors';
11
11
  import { exec } from 'child_process';
12
12
  import { fileURLToPath } from 'url';
13
13
  import { getActiveProject, getConfig, saveConfig } from './config.js';
14
+ import { attachExternalBridges } from '../mcps/ExternalBridge.js';
14
15
 
15
16
  const __filename = fileURLToPath(import.meta.url);
16
17
  const __dirname = path.dirname(__filename);
@@ -60,7 +61,6 @@ function isBinaryData(buffer) {
60
61
  }
61
62
 
62
63
  // --- CENTRAL DEBOUNCER ---
63
- // Called by local file changes AND MCP events. It resets the 15s timer.
64
64
  function triggerDebouncedSync() {
65
65
  const activeProject = getActiveProject();
66
66
  if (!activeProject || !activeProject.path) return;
@@ -71,13 +71,46 @@ function triggerDebouncedSync() {
71
71
  }, INACTIVITY_DELAY_MS);
72
72
  }
73
73
 
74
+ // --- EXTERNAL MCP INITIALIZATION (ON BOOT / ON CONNECT) ---
75
+ async function fetchInitialMCPContext(server) {
76
+ try {
77
+ const res = await fetch(server.url, {
78
+ method: 'POST',
79
+ headers: { 'Content-Type': 'application/json' },
80
+ body: JSON.stringify({
81
+ jsonrpc: "2.0",
82
+ method: "tools/call",
83
+ params: { name: "get_initial_state", arguments: {} },
84
+ id: Date.now()
85
+ })
86
+ });
87
+
88
+ if (res.ok) {
89
+ const data = await res.json();
90
+ if (data.result && data.result.content && data.result.content.length > 0) {
91
+ const stateText = data.result.content[0].text;
92
+
93
+ fileActivityBuffer += `\n[${new Date().toLocaleTimeString()}][MCP BOOT STATE] Source: ${server.name} | Current Context:\n${stateText}\n`;
94
+ broadcastLocalLog('mcp', `📥 Fetched initial project state from ${server.name}`);
95
+
96
+ triggerDebouncedSync();
97
+ }
98
+ }
99
+ } catch (e) {
100
+ broadcastLocalLog('error', `⚠️ Failed to fetch initial state from ${server.name}. Is it running?`);
101
+ }
102
+ }
103
+
74
104
  export async function startDaemon(preferredPort) {
75
105
  const actualPort = await findAvailablePort(preferredPort);
106
+
76
107
  const app = express();
77
108
 
109
+ attachExternalBridges(app);
110
+
78
111
  const corsOptionsDelegate = (req, callback) => {
79
112
  const origin = req.header('Origin');
80
- const allowedWebApps = ['https://thrust.web.app', 'http://localhost:3000'];
113
+ const allowedWebApps =['https://thrust.web.app', 'http://localhost:3000'];
81
114
  if (req.path.startsWith('/api/mcp')) {
82
115
  callback(null, { origin: true });
83
116
  } else if (req.path === '/api/auth/callback') {
@@ -119,6 +152,7 @@ export async function startDaemon(preferredPort) {
119
152
  projectId,
120
153
  projectPath: config.leads[projectId].path,
121
154
  activeThrust: data.length > 0 ? data[0] : null
155
+ //timeline
122
156
  });
123
157
  broadcastLocalLog('mcp', `🔗 [Context Sync] External client requested project state.`);
124
158
  } catch (e) {
@@ -135,7 +169,6 @@ export async function startDaemon(preferredPort) {
135
169
 
136
170
  broadcastLocalLog('mcp', `🔗 [${source} Pushed Event] ${action_type}: ${description}`);
137
171
 
138
- // Add to buffer and reset the 15-second countdown
139
172
  fileActivityBuffer += `\n[${new Date().toLocaleTimeString()}] [MCP EXT EVENT] Source: ${source} | Action: ${action_type} | Desc: ${description} | Needs Code Sync: ${requires_code_sync ? 'Yes' : 'No'}\n`;
140
173
  triggerDebouncedSync();
141
174
 
@@ -147,7 +180,7 @@ export async function startDaemon(preferredPort) {
147
180
  // ==========================================
148
181
 
149
182
  app.get('/api/mcp/servers', (req, res) => {
150
- res.json(getConfig().mcpServers || []);
183
+ res.json(getConfig().mcpServers ||[]);
151
184
  });
152
185
 
153
186
  app.post('/api/mcp/servers', (req, res) => {
@@ -155,11 +188,12 @@ export async function startDaemon(preferredPort) {
155
188
  if (!name || !url) return res.status(400).json({ error: "Missing name or url" });
156
189
 
157
190
  const config = getConfig();
158
- if (!config.mcpServers) config.mcpServers = [];
191
+ if (!config.mcpServers) config.mcpServers =[];
159
192
  config.mcpServers.push({ name, url, type: 'http' });
160
193
  saveConfig(config);
161
194
 
162
195
  pollExternalMCPServers();
196
+ fetchInitialMCPContext({ name, url, type: 'http' }); // Fetch state immediately upon linking
163
197
  res.json({ success: true });
164
198
  });
165
199
 
@@ -208,8 +242,7 @@ export async function startDaemon(preferredPort) {
208
242
 
209
243
  broadcastLocalLog('mcp', `⚡ Queried ${serverName} for ${toolName}.`);
210
244
 
211
- // Add the query result to the buffer and reset the 15-second countdown
212
- fileActivityBuffer += `\n[${new Date().toLocaleTimeString()}] [MCP DIRECT QUERY RESULT] Source: ${serverName} | Tool: ${toolName} | Target: ${targetArg || 'none'} \nResult:\n${resultText}\n`;
245
+ fileActivityBuffer += `\n[${new Date().toLocaleTimeString()}][MCP DIRECT QUERY RESULT] Source: ${serverName} | Tool: ${toolName} | Target: ${targetArg || 'none'} \nResult:\n${resultText}\n`;
213
246
  triggerDebouncedSync();
214
247
 
215
248
  res.json({ success: true, data: resultText });
@@ -288,7 +321,6 @@ export async function startDaemon(preferredPort) {
288
321
  if (!response.ok) throw new Error(data.error);
289
322
 
290
323
  if (config.leads[projectId]?.path) {
291
- // Instantly sync when a task is manually completed to update cloud state fast
292
324
  if (inactivityTimer) clearTimeout(inactivityTimer);
293
325
  syncContext(config.leads[projectId].path);
294
326
  }
@@ -375,8 +407,6 @@ export async function startDaemon(preferredPort) {
375
407
  saveConfig(config);
376
408
 
377
409
  await startWatching(folderPath);
378
-
379
- // Initial sync on link
380
410
  triggerDebouncedSync();
381
411
 
382
412
  res.json({ success: true });
@@ -481,7 +511,7 @@ async function pollExternalMCPServers() {
481
511
  }
482
512
  }
483
513
  } catch (e) {
484
- // Silently fail to avoid spamming the logs if Unity is temporarily closed
514
+ // Silently fail to avoid spamming the logs
485
515
  }
486
516
  }
487
517
 
@@ -498,15 +528,21 @@ async function startWatching(projectPath) {
498
528
 
499
529
  fileActivityBuffer = "";
500
530
 
531
+ // Fetch initial MCP states immediately upon watching a project
532
+ const config = getConfig();
533
+ if (config.mcpServers && config.mcpServers.length > 0) {
534
+ config.mcpServers.forEach(server => fetchInitialMCPContext(server));
535
+ }
536
+
501
537
  // Start the active polling loop for external services
502
538
  if (mcpPollTimer) clearInterval(mcpPollTimer);
503
539
  mcpPollTimer = setInterval(pollExternalMCPServers, MCP_POLL_INTERVAL_MS);
504
540
 
505
541
  currentWatcher = chokidar.watch(projectPath, {
506
- ignored: [
507
- /(^|[\/\\])\../,
542
+ ignored:[
543
+ /(^|[\/\\])\../,
508
544
  '**/node_modules/**', '**/dist/**', '**/build/**',
509
- // Ignore noisy Unity cache & build folders to prevent infinite debounce loops
545
+ // Ignore noisy Unity cache & build folders
510
546
  '**/Library/**', '**/Temp/**', '**/Logs/**', '**/obj/**', '**/ProjectSettings/**'
511
547
  ],
512
548
  persistent: true,
@@ -542,9 +578,34 @@ function connectWebSocket() {
542
578
  wsRetryLogged = false;
543
579
  });
544
580
 
545
- globalWs.on('message', (data) => {
581
+ globalWs.on('message', async (data) => {
546
582
  try {
547
583
  const msg = JSON.parse(data.toString());
584
+
585
+ // NEW: Handle dynamic MCP Queries pushed from the AI Director
586
+ if (msg.type === 'mcp_query' && msg.payload) {
587
+ const { serverName, toolName, targetArg } = msg.payload;
588
+ const targetServer = getConfig().mcpServers?.find(s => s.name.toLowerCase() === serverName.toLowerCase());
589
+
590
+ if (targetServer) {
591
+ broadcastLocalLog('mcp', `🤖 AI requested data from ${serverName} (${toolName})...`);
592
+ try {
593
+ const response = await fetch(targetServer.url, {
594
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
595
+ body: JSON.stringify({ jsonrpc: "2.0", method: "tools/call", params: { name: toolName, arguments: targetArg ? { target: targetArg } : {} }, id: Date.now() })
596
+ });
597
+ if (response.ok) {
598
+ const responseData = await response.json();
599
+ const resultText = responseData.result?.content?.[0]?.text || "No data returned.";
600
+ fileActivityBuffer += `\n[${new Date().toLocaleTimeString()}] [MCP AI QUERY RESULT] Source: ${serverName} | Tool: ${toolName} | Target: ${targetArg || 'none'} \nResult:\n${resultText}\n`;
601
+ triggerDebouncedSync();
602
+ }
603
+ } catch (e) {
604
+ broadcastLocalLog('error', `⚠️ AI failed to query MCP server: ${serverName}`);
605
+ }
606
+ }
607
+ }
608
+
548
609
  if (msg.type === 'toast' || msg.type === 'response') {
549
610
  broadcastLocalLog('ai', `🔔 [AI]: ${msg.message || msg.text}`);
550
611
  }
@@ -573,7 +634,7 @@ async function syncContext(projectPath) {
573
634
 
574
635
  let diff = "";
575
636
  let newFilesData = "";
576
- let imagesData = [];
637
+ let imagesData =[];
577
638
 
578
639
  try {
579
640
  const git = simpleGit(projectPath);
@@ -636,5 +697,5 @@ async function syncContext(projectPath) {
636
697
  }));
637
698
 
638
699
  broadcastLocalLog('sync', `✅ Context Batch synced to AI.`);
639
- fileActivityBuffer = "";
700
+ fileActivityBuffer = "";
640
701
  }