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.
- package/README.md +1 -0
- package/frontend/index.html +232 -37
- package/index.js +35 -11
- package/mcps/ExternalBridge.js +144 -0
- package/mcps/ThrustMCPBridge.cs +78 -24
- package/mcps/core.js +19 -0
- package/mcps/projectMcpServer.js +165 -0
- package/package.json +3 -1
- package/utils/daemon.js +90 -22
|
@@ -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
|
+
}
|
package/mcps/ThrustMCPBridge.cs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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 >
|
|
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]
|
|
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
|
-
|
|
111
|
-
|
|
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"":
|
|
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;
|
|
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.
|
|
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",
|