@slack/radar-mcp 1.1.0 → 1.1.2
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/dist/mcp/index.js +4 -2
- package/dist/shared/android.d.ts +14 -0
- package/dist/shared/android.js +101 -15
- package/dist/shared/transport.d.ts +7 -0
- package/dist/web/public/index.html +64 -30
- package/dist/web/server.js +6 -2
- package/package.json +2 -1
package/dist/mcp/index.js
CHANGED
|
@@ -101,8 +101,10 @@ async function handleSessionEnable(args) {
|
|
|
101
101
|
const forwarded = device.ensureForward();
|
|
102
102
|
if (!forwarded) {
|
|
103
103
|
sessionEnabled = false;
|
|
104
|
+
const reason = device.getLastForwardError?.() ?? null;
|
|
104
105
|
return textResult({
|
|
105
|
-
error:
|
|
106
|
+
error: reason ??
|
|
107
|
+
"Port forwarding failed. Ensure a device is connected.",
|
|
106
108
|
enabled: false,
|
|
107
109
|
platform: device.platform,
|
|
108
110
|
}, true);
|
|
@@ -291,7 +293,7 @@ function buildApiPath(name, args) {
|
|
|
291
293
|
}
|
|
292
294
|
}
|
|
293
295
|
// --- MCP Server ---
|
|
294
|
-
const server = new Server({ name: "slack-radar", version: "1.1.
|
|
296
|
+
const server = new Server({ name: "slack-radar", version: "1.1.2" }, { capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS });
|
|
295
297
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
296
298
|
tools: TOOL_DEFINITIONS,
|
|
297
299
|
}));
|
package/dist/shared/android.d.ts
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import type { DeviceTransport } from "./transport.js";
|
|
2
|
+
/**
|
|
3
|
+
* Resolve the adb binary. MCP servers spawned from GUI launchers (Spotlight,
|
|
4
|
+
* Raycast, Dock) do not inherit the user's shell PATH, so a bare `adb` call
|
|
5
|
+
* fails with ENOENT even when adb works fine in the user's terminal. We look
|
|
6
|
+
* in PATH first, then fall back to common Android SDK install locations.
|
|
7
|
+
*
|
|
8
|
+
* Returns either an absolute path to the adb binary or the string "adb"
|
|
9
|
+
* (PATH-relative). When all fallbacks miss, returns null so callers can
|
|
10
|
+
* surface a clear error instead of a misleading "device not connected".
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveAdbPath(): string | null;
|
|
2
13
|
/**
|
|
3
14
|
* Parse the output of `ps -A -o USER,NAME` to extract the Android user ID
|
|
4
15
|
* for the debug build process. When multiple profiles are running, prefers
|
|
@@ -12,11 +23,14 @@ export declare class AndroidTransport implements DeviceTransport {
|
|
|
12
23
|
private detectedUserId;
|
|
13
24
|
private currentTimeoutMinutes;
|
|
14
25
|
private radarEnabled;
|
|
26
|
+
private lastForwardError;
|
|
15
27
|
ensureForward(): boolean;
|
|
28
|
+
getLastForwardError(): string | null;
|
|
16
29
|
activate(timeoutMinutes: number): void;
|
|
17
30
|
resetState(): void;
|
|
18
31
|
isForwarded(): boolean;
|
|
19
32
|
getTimeoutMinutes(): number;
|
|
33
|
+
getActiveUserId(): string | null;
|
|
20
34
|
screenshot(fullRes: boolean): string;
|
|
21
35
|
recordScreen(durationS: number): string;
|
|
22
36
|
getAppState(): string;
|
package/dist/shared/android.js
CHANGED
|
@@ -1,9 +1,62 @@
|
|
|
1
1
|
import { execSync } from "child_process";
|
|
2
|
+
import fs from "fs";
|
|
2
3
|
import os from "os";
|
|
3
4
|
import path from "path";
|
|
4
5
|
const RADAR_PORT = 8099;
|
|
5
6
|
const SOCKET_NAME = "slack-radar";
|
|
6
7
|
const ENABLE_ACTION = "slack.debug.ENABLE_RADAR";
|
|
8
|
+
/**
|
|
9
|
+
* Resolve the adb binary. MCP servers spawned from GUI launchers (Spotlight,
|
|
10
|
+
* Raycast, Dock) do not inherit the user's shell PATH, so a bare `adb` call
|
|
11
|
+
* fails with ENOENT even when adb works fine in the user's terminal. We look
|
|
12
|
+
* in PATH first, then fall back to common Android SDK install locations.
|
|
13
|
+
*
|
|
14
|
+
* Returns either an absolute path to the adb binary or the string "adb"
|
|
15
|
+
* (PATH-relative). When all fallbacks miss, returns null so callers can
|
|
16
|
+
* surface a clear error instead of a misleading "device not connected".
|
|
17
|
+
*/
|
|
18
|
+
export function resolveAdbPath() {
|
|
19
|
+
// 1. PATH-relative: if `adb` resolves, use it as-is (fast path).
|
|
20
|
+
try {
|
|
21
|
+
const which = execSync("command -v adb", {
|
|
22
|
+
timeout: 2000,
|
|
23
|
+
encoding: "utf-8",
|
|
24
|
+
shell: "/bin/sh",
|
|
25
|
+
}).trim();
|
|
26
|
+
if (which && fs.existsSync(which))
|
|
27
|
+
return "adb";
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// `command -v adb` returns non-zero when not found; fall through
|
|
31
|
+
}
|
|
32
|
+
// 2. Common install locations, in order of likelihood on macOS and Linux.
|
|
33
|
+
const home = os.homedir();
|
|
34
|
+
const candidates = [
|
|
35
|
+
process.env.ANDROID_HOME
|
|
36
|
+
? path.join(process.env.ANDROID_HOME, "platform-tools", "adb")
|
|
37
|
+
: null,
|
|
38
|
+
process.env.ANDROID_SDK_ROOT
|
|
39
|
+
? path.join(process.env.ANDROID_SDK_ROOT, "platform-tools", "adb")
|
|
40
|
+
: null,
|
|
41
|
+
path.join(home, "Library", "Android", "sdk", "platform-tools", "adb"),
|
|
42
|
+
path.join(home, "Android", "Sdk", "platform-tools", "adb"),
|
|
43
|
+
"/usr/local/bin/adb",
|
|
44
|
+
"/opt/homebrew/bin/adb",
|
|
45
|
+
].filter((p) => p !== null);
|
|
46
|
+
for (const candidate of candidates) {
|
|
47
|
+
try {
|
|
48
|
+
if (fs.existsSync(candidate))
|
|
49
|
+
return candidate;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Permission denied on some directory; skip
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
// Module-level resolution, done once at import time. The MCP process lifetime
|
|
58
|
+
// is short enough (session-scoped) that we do not need to re-resolve.
|
|
59
|
+
const ADB = resolveAdbPath();
|
|
7
60
|
/**
|
|
8
61
|
* Parse the output of `ps -A -o USER,NAME` to extract the Android user ID
|
|
9
62
|
* for the debug build process. When multiple profiles are running, prefers
|
|
@@ -33,27 +86,40 @@ export class AndroidTransport {
|
|
|
33
86
|
detectedUserId = null;
|
|
34
87
|
currentTimeoutMinutes = 5;
|
|
35
88
|
radarEnabled = false;
|
|
89
|
+
lastForwardError = null;
|
|
36
90
|
ensureForward() {
|
|
37
91
|
if (this.portForwarded)
|
|
38
92
|
return true;
|
|
93
|
+
if (!ADB) {
|
|
94
|
+
this.lastForwardError =
|
|
95
|
+
"adb binary not found. Set ANDROID_HOME or install Android SDK platform-tools. Searched: PATH, $ANDROID_HOME/platform-tools/adb, ~/Library/Android/sdk/platform-tools/adb, ~/Android/Sdk/platform-tools/adb, /usr/local/bin/adb, /opt/homebrew/bin/adb.";
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
39
98
|
try {
|
|
40
|
-
execSync(
|
|
99
|
+
execSync(`${ADB} forward tcp:${RADAR_PORT} localabstract:${SOCKET_NAME}`, {
|
|
41
100
|
timeout: 5000,
|
|
42
101
|
});
|
|
43
102
|
this.portForwarded = true;
|
|
103
|
+
this.lastForwardError = null;
|
|
44
104
|
return true;
|
|
45
105
|
}
|
|
46
|
-
catch {
|
|
106
|
+
catch (e) {
|
|
107
|
+
this.lastForwardError = e.message;
|
|
47
108
|
return false;
|
|
48
109
|
}
|
|
49
110
|
}
|
|
111
|
+
getLastForwardError() {
|
|
112
|
+
return this.lastForwardError;
|
|
113
|
+
}
|
|
50
114
|
activate(timeoutMinutes) {
|
|
115
|
+
if (!ADB)
|
|
116
|
+
return;
|
|
51
117
|
const timeout = timeoutMinutes || this.currentTimeoutMinutes;
|
|
52
118
|
if (this.radarEnabled && timeout === this.currentTimeoutMinutes)
|
|
53
119
|
return;
|
|
54
120
|
try {
|
|
55
121
|
const userId = this.detectDebugBuildUser();
|
|
56
|
-
execSync(
|
|
122
|
+
execSync(`${ADB} shell am broadcast --user ${userId} -a ${ENABLE_ACTION} --ei timeout_minutes ${timeout}`, { timeout: 5000 });
|
|
57
123
|
this.currentTimeoutMinutes = timeout;
|
|
58
124
|
this.radarEnabled = true;
|
|
59
125
|
}
|
|
@@ -65,6 +131,7 @@ export class AndroidTransport {
|
|
|
65
131
|
this.portForwarded = false;
|
|
66
132
|
this.detectedUserId = null;
|
|
67
133
|
this.radarEnabled = false;
|
|
134
|
+
this.lastForwardError = null;
|
|
68
135
|
}
|
|
69
136
|
isForwarded() {
|
|
70
137
|
return this.portForwarded;
|
|
@@ -72,12 +139,17 @@ export class AndroidTransport {
|
|
|
72
139
|
getTimeoutMinutes() {
|
|
73
140
|
return this.currentTimeoutMinutes;
|
|
74
141
|
}
|
|
142
|
+
getActiveUserId() {
|
|
143
|
+
return this.detectedUserId;
|
|
144
|
+
}
|
|
75
145
|
screenshot(fullRes) {
|
|
146
|
+
if (!ADB)
|
|
147
|
+
throw new Error("adb not found on PATH or common SDK locations");
|
|
76
148
|
const tmpFile = path.join(os.tmpdir(), `slack-radar-screenshot-${Date.now()}.png`);
|
|
77
149
|
const devicePath = "/data/local/tmp/slack-radar-screenshot.png";
|
|
78
|
-
execSync(
|
|
79
|
-
execSync(
|
|
80
|
-
execSync(
|
|
150
|
+
execSync(`${ADB} shell screencap -p ${devicePath}`, { timeout: 10000 });
|
|
151
|
+
execSync(`${ADB} pull ${devicePath} "${tmpFile}"`, { timeout: 10000 });
|
|
152
|
+
execSync(`${ADB} shell rm ${devicePath}`, { timeout: 5000 });
|
|
81
153
|
if (!fullRes) {
|
|
82
154
|
try {
|
|
83
155
|
execSync(`sips -Z 720 "${tmpFile}" 2>/dev/null`, { timeout: 5000 });
|
|
@@ -89,21 +161,27 @@ export class AndroidTransport {
|
|
|
89
161
|
return tmpFile;
|
|
90
162
|
}
|
|
91
163
|
recordScreen(durationS) {
|
|
164
|
+
if (!ADB)
|
|
165
|
+
throw new Error("adb not found on PATH or common SDK locations");
|
|
92
166
|
const duration = Math.min(Math.max(durationS, 1), 30);
|
|
93
167
|
const tmpFile = path.join(os.tmpdir(), `slack-radar-recording-${Date.now()}.mp4`);
|
|
94
168
|
const devicePath = "/data/local/tmp/slack-radar-recording.mp4";
|
|
95
|
-
execSync(
|
|
96
|
-
execSync(
|
|
97
|
-
execSync(
|
|
169
|
+
execSync(`${ADB} shell screenrecord --time-limit ${duration} --size 720x1280 ${devicePath}`, { timeout: (duration + 5) * 1000 });
|
|
170
|
+
execSync(`${ADB} pull ${devicePath} "${tmpFile}"`, { timeout: 10000 });
|
|
171
|
+
execSync(`${ADB} shell rm ${devicePath}`, { timeout: 5000 });
|
|
98
172
|
return tmpFile;
|
|
99
173
|
}
|
|
100
174
|
getAppState() {
|
|
101
|
-
|
|
175
|
+
if (!ADB)
|
|
176
|
+
throw new Error("adb not found on PATH or common SDK locations");
|
|
177
|
+
return execSync(`${ADB} shell "dumpsys activity activities | grep -A 5 'com.Slack.internal'"`, { timeout: 10000, encoding: "utf-8" }).trim();
|
|
102
178
|
}
|
|
103
179
|
getRecentLogs(tag, limit, grep) {
|
|
104
|
-
|
|
180
|
+
if (!ADB)
|
|
181
|
+
throw new Error("adb not found on PATH or common SDK locations");
|
|
182
|
+
let cmd = `${ADB} logcat -d | grep -F "${tag}" | tail -${limit}`;
|
|
105
183
|
if (grep) {
|
|
106
|
-
cmd =
|
|
184
|
+
cmd = `${ADB} logcat -d | grep -F "${tag}" | grep -i "${grep.replace(/"/g, '\\"')}" | tail -${limit}`;
|
|
107
185
|
}
|
|
108
186
|
const output = execSync(cmd, {
|
|
109
187
|
timeout: 10000,
|
|
@@ -113,8 +191,10 @@ export class AndroidTransport {
|
|
|
113
191
|
return output.trim().split("\n").filter(Boolean);
|
|
114
192
|
}
|
|
115
193
|
listProfiles() {
|
|
194
|
+
if (!ADB)
|
|
195
|
+
return [{ userId: "0", label: "Personal (user 0)" }];
|
|
116
196
|
try {
|
|
117
|
-
const ps = execSync(
|
|
197
|
+
const ps = execSync(`${ADB} shell "ps -A -o USER,NAME 2>/dev/null | grep Slack | grep debug"`, { timeout: 5000, encoding: "utf-8" }).trim();
|
|
118
198
|
if (!ps)
|
|
119
199
|
return [{ userId: "0", label: "Personal (user 0)" }];
|
|
120
200
|
const seen = new Set();
|
|
@@ -139,8 +219,10 @@ export class AndroidTransport {
|
|
|
139
219
|
}
|
|
140
220
|
}
|
|
141
221
|
activateForUser(userId, timeoutMinutes) {
|
|
222
|
+
if (!ADB)
|
|
223
|
+
throw new Error("adb not found on PATH or common SDK locations");
|
|
142
224
|
const timeout = timeoutMinutes || this.currentTimeoutMinutes;
|
|
143
|
-
execSync(
|
|
225
|
+
execSync(`${ADB} shell am broadcast --user ${userId} -a ${ENABLE_ACTION} --ei timeout_minutes ${timeout}`, { timeout: 5000 });
|
|
144
226
|
this.detectedUserId = userId;
|
|
145
227
|
this.currentTimeoutMinutes = timeout;
|
|
146
228
|
this.radarEnabled = true;
|
|
@@ -148,8 +230,12 @@ export class AndroidTransport {
|
|
|
148
230
|
detectDebugBuildUser() {
|
|
149
231
|
if (this.detectedUserId !== null)
|
|
150
232
|
return this.detectedUserId;
|
|
233
|
+
if (!ADB) {
|
|
234
|
+
this.detectedUserId = "0";
|
|
235
|
+
return this.detectedUserId;
|
|
236
|
+
}
|
|
151
237
|
try {
|
|
152
|
-
const ps = execSync(
|
|
238
|
+
const ps = execSync(`${ADB} shell "ps -A -o USER,NAME 2>/dev/null | grep Slack | grep debug"`, { timeout: 5000, encoding: "utf-8" }).trim();
|
|
153
239
|
this.detectedUserId = parseUserFromPsOutput(ps);
|
|
154
240
|
}
|
|
155
241
|
catch {
|
|
@@ -55,4 +55,11 @@ export interface DeviceTransport {
|
|
|
55
55
|
* @returns Array of matching log lines
|
|
56
56
|
*/
|
|
57
57
|
getRecentLogs(tag: string, limit: number, grep?: string): string[];
|
|
58
|
+
/**
|
|
59
|
+
* Returns the last error message from an `ensureForward()` failure, or null
|
|
60
|
+
* if the last attempt succeeded or has not happened. Lets callers surface
|
|
61
|
+
* concrete causes (missing adb, unauthorized device) instead of a generic
|
|
62
|
+
* "device not connected" message. Optional; implementations may omit it.
|
|
63
|
+
*/
|
|
64
|
+
getLastForwardError?(): string | null;
|
|
58
65
|
}
|
|
@@ -223,8 +223,9 @@
|
|
|
223
223
|
|
|
224
224
|
<div class="toolbar">
|
|
225
225
|
<input type="text" id="filter" placeholder="Filter by URL, channel, event name, payload..." />
|
|
226
|
-
<
|
|
226
|
+
<span id="profile-label" style="display: none; font-size: 12px; color: #616061; margin-right: 4px;"></span>
|
|
227
227
|
<button id="enable-btn" style="display: none;">Enable Radar</button>
|
|
228
|
+
<button id="disconnect-btn" style="display: none;">Disconnect</button>
|
|
228
229
|
<button id="pause-btn">Pause</button>
|
|
229
230
|
<button id="clear-btn">Clear</button>
|
|
230
231
|
</div>
|
|
@@ -256,7 +257,8 @@
|
|
|
256
257
|
const detailEl = document.getElementById("detail");
|
|
257
258
|
const filterEl = document.getElementById("filter");
|
|
258
259
|
const enableBtn = document.getElementById("enable-btn");
|
|
259
|
-
const
|
|
260
|
+
const profileLabel = document.getElementById("profile-label");
|
|
261
|
+
const disconnectBtn = document.getElementById("disconnect-btn");
|
|
260
262
|
const pauseBtn = document.getElementById("pause-btn");
|
|
261
263
|
const clearBtn = document.getElementById("clear-btn");
|
|
262
264
|
const statusDot = document.getElementById("status-dot");
|
|
@@ -307,22 +309,25 @@
|
|
|
307
309
|
enableBtn.disabled = true;
|
|
308
310
|
enableBtn.textContent = "Enabling…";
|
|
309
311
|
try {
|
|
310
|
-
const
|
|
311
|
-
const qs = selectedUser ? `?user_id=${selectedUser}` : "";
|
|
312
|
-
const res = await fetch(`/_control/activate${qs}`, { method: "POST" });
|
|
312
|
+
const res = await fetch("/_control/activate", { method: "POST" });
|
|
313
313
|
const data = await res.json();
|
|
314
314
|
if (!data.ok) {
|
|
315
315
|
statusText.textContent = "enable failed";
|
|
316
316
|
alert(data.error || "Failed to enable radar");
|
|
317
317
|
} else {
|
|
318
318
|
statusText.textContent = "connecting";
|
|
319
|
+
// Device-side server needs a moment to start after the broadcast.
|
|
320
|
+
// Without this delay, the SSE connect races the server startup and
|
|
321
|
+
// gets connection-refused, snapping back to disconnected instantly.
|
|
322
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
323
|
+
connectStream();
|
|
324
|
+
return; // skip the finally reset — stay in "connecting" state
|
|
319
325
|
}
|
|
320
326
|
} catch (e) {
|
|
321
327
|
alert("Failed to reach server: " + e.message);
|
|
322
|
-
} finally {
|
|
323
|
-
enableBtn.disabled = false;
|
|
324
|
-
enableBtn.textContent = "Enable Radar";
|
|
325
328
|
}
|
|
329
|
+
enableBtn.disabled = false;
|
|
330
|
+
enableBtn.textContent = "Enable Radar";
|
|
326
331
|
});
|
|
327
332
|
|
|
328
333
|
async function fetchProfiles() {
|
|
@@ -330,32 +335,47 @@
|
|
|
330
335
|
const res = await fetch("/_control/profiles");
|
|
331
336
|
const data = await res.json();
|
|
332
337
|
const profiles = data.profiles || [];
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
338
|
+
const activeId = data.active_user_id;
|
|
339
|
+
|
|
340
|
+
if (activeId && profiles.length >= 1) {
|
|
341
|
+
const active = profiles.find((p) => p.userId === activeId) ?? profiles[0];
|
|
342
|
+
profileLabel.textContent = active.label;
|
|
343
|
+
profileLabel.style.display = "";
|
|
344
|
+
} else if (profiles.length >= 1) {
|
|
345
|
+
profileLabel.textContent = profiles[0].label;
|
|
346
|
+
profileLabel.style.display = "";
|
|
342
347
|
} else {
|
|
343
|
-
|
|
348
|
+
profileLabel.style.display = "none";
|
|
344
349
|
}
|
|
345
350
|
} catch {
|
|
346
|
-
|
|
351
|
+
profileLabel.style.display = "none";
|
|
347
352
|
}
|
|
348
353
|
}
|
|
349
354
|
|
|
350
355
|
function setDisconnectedUi(disconnected) {
|
|
351
356
|
enableBtn.style.display = disconnected ? "" : "none";
|
|
357
|
+
disconnectBtn.style.display = disconnected ? "none" : "";
|
|
352
358
|
if (disconnected) {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
profileSelect.style.display = "none";
|
|
359
|
+
uptimeBadge.textContent = "";
|
|
360
|
+
extendBtn.style.display = "none";
|
|
356
361
|
}
|
|
362
|
+
fetchProfiles();
|
|
357
363
|
}
|
|
358
364
|
|
|
365
|
+
disconnectBtn.addEventListener("click", () => {
|
|
366
|
+
if (activeStream) {
|
|
367
|
+
activeStream.close();
|
|
368
|
+
activeStream = null;
|
|
369
|
+
}
|
|
370
|
+
state.connected = false;
|
|
371
|
+
statusDot.classList.remove("connected");
|
|
372
|
+
statusText.textContent = "disconnected";
|
|
373
|
+
enableBtn.disabled = false;
|
|
374
|
+
enableBtn.textContent = "Enable Radar";
|
|
375
|
+
setDisconnectedUi(true);
|
|
376
|
+
renderList();
|
|
377
|
+
});
|
|
378
|
+
|
|
359
379
|
// --- SSE ---
|
|
360
380
|
function insertReconnectMarker() {
|
|
361
381
|
const marker = {
|
|
@@ -363,14 +383,18 @@
|
|
|
363
383
|
timestamp: Date.now(),
|
|
364
384
|
label: `reconnected @ ${new Date().toLocaleTimeString("en-US", { hour12: false })}`,
|
|
365
385
|
};
|
|
386
|
+
const buffers = state.buffers;
|
|
366
387
|
for (const type of ["network", "rtm", "clog"]) {
|
|
367
|
-
const buf =
|
|
388
|
+
const buf = buffers[type];
|
|
368
389
|
if (buf.length > 0) buf.push({ ...marker }); // avoid empty-list markers
|
|
369
390
|
}
|
|
370
391
|
}
|
|
371
392
|
|
|
393
|
+
let activeStream = null;
|
|
394
|
+
|
|
372
395
|
function connectStream() {
|
|
373
396
|
const es = new EventSource("/api/stream");
|
|
397
|
+
activeStream = es;
|
|
374
398
|
es.onopen = () => {
|
|
375
399
|
const wasDisconnected = !state.connected && state.hadConnection;
|
|
376
400
|
state.connected = true;
|
|
@@ -394,15 +418,21 @@
|
|
|
394
418
|
state.connected = false;
|
|
395
419
|
statusDot.classList.remove("connected");
|
|
396
420
|
statusText.textContent = "disconnected";
|
|
421
|
+
enableBtn.disabled = false;
|
|
422
|
+
enableBtn.textContent = "Enable Radar";
|
|
397
423
|
setDisconnectedUi(true);
|
|
398
424
|
renderList();
|
|
399
425
|
es.close();
|
|
400
|
-
setTimeout(connectStream, 2000);
|
|
401
426
|
};
|
|
402
427
|
}
|
|
403
428
|
|
|
404
429
|
function ingest(type, event) {
|
|
405
|
-
|
|
430
|
+
// Attribute incoming events to whichever profile is currently active.
|
|
431
|
+
// There is a small misattribution window right at a switch boundary
|
|
432
|
+
// (a few events in flight may land in the wrong bucket), which is an
|
|
433
|
+
// acceptable cost relative to server-side event tagging.
|
|
434
|
+
const buffers = state.buffers;
|
|
435
|
+
const buf = buffers[type];
|
|
406
436
|
if (!buf) return;
|
|
407
437
|
buf.push(event);
|
|
408
438
|
if (buf.length > state.MAX_BUFFER) buf.shift();
|
|
@@ -411,9 +441,10 @@
|
|
|
411
441
|
}
|
|
412
442
|
|
|
413
443
|
function updateCounts() {
|
|
414
|
-
|
|
415
|
-
document.getElementById("
|
|
416
|
-
document.getElementById("
|
|
444
|
+
const buffers = state.buffers;
|
|
445
|
+
document.getElementById("network-count").textContent = buffers.network.length;
|
|
446
|
+
document.getElementById("rtm-count").textContent = buffers.rtm.length;
|
|
447
|
+
document.getElementById("clog-count").textContent = buffers.clog.length;
|
|
417
448
|
}
|
|
418
449
|
|
|
419
450
|
// --- Rendering ---
|
|
@@ -665,6 +696,7 @@ ${escapeHtml(tryPretty(data.data))}
|
|
|
665
696
|
});
|
|
666
697
|
|
|
667
698
|
async function pollPing() {
|
|
699
|
+
if (!state.connected) return;
|
|
668
700
|
try {
|
|
669
701
|
const res = await fetch("/api/ping");
|
|
670
702
|
const data = await res.json();
|
|
@@ -683,11 +715,13 @@ ${escapeHtml(tryPretty(data.data))}
|
|
|
683
715
|
extendBtn.style.display = "none";
|
|
684
716
|
}
|
|
685
717
|
}
|
|
686
|
-
pollPing
|
|
718
|
+
// pollPing runs on interval; skips when not connected.
|
|
687
719
|
setInterval(pollPing, 5000);
|
|
688
720
|
|
|
689
721
|
// --- Go ---
|
|
690
|
-
|
|
722
|
+
// Start in disconnected state. User picks a profile and clicks Enable.
|
|
723
|
+
// connectStream() is called after Enable succeeds.
|
|
724
|
+
setDisconnectedUi(true);
|
|
691
725
|
updateCounts();
|
|
692
726
|
renderList();
|
|
693
727
|
</script>
|
package/dist/web/server.js
CHANGED
|
@@ -43,10 +43,14 @@ export function createServer() {
|
|
|
43
43
|
}
|
|
44
44
|
function listProfiles(res) {
|
|
45
45
|
const profiles = transport.listProfiles();
|
|
46
|
+
const activeUserId = transport.getActiveUserId();
|
|
46
47
|
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
47
|
-
res.end(JSON.stringify({ profiles }));
|
|
48
|
+
res.end(JSON.stringify({ profiles, active_user_id: activeUserId }));
|
|
48
49
|
}
|
|
49
50
|
function activateRadar(res, userId) {
|
|
51
|
+
// Reset cached state so ensureForward() re-runs adb forward. This handles
|
|
52
|
+
// stale forwards after device-side idle timeout or profile switching.
|
|
53
|
+
transport.resetState();
|
|
50
54
|
const forwarded = transport.ensureForward();
|
|
51
55
|
if (!forwarded) {
|
|
52
56
|
res.writeHead(502, { "Content-Type": "application/json; charset=utf-8" });
|
|
@@ -73,7 +77,7 @@ function activateRadar(res, userId) {
|
|
|
73
77
|
return;
|
|
74
78
|
}
|
|
75
79
|
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
76
|
-
res.end(JSON.stringify({ ok: true, user_id: userId ?? "auto" }));
|
|
80
|
+
res.end(JSON.stringify({ ok: true, user_id: userId ?? transport.getActiveUserId() ?? "auto" }));
|
|
77
81
|
}
|
|
78
82
|
function proxyToRadar(req, res) {
|
|
79
83
|
const upstream = http.request({
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slack/radar-mcp",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "MCP server and web dashboard for on-device debugging of the Slack Android app via ADB",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/mcp/index.js",
|
|
7
7
|
"bin": {
|
|
8
|
+
"radar-mcp": "dist/mcp/index.js",
|
|
8
9
|
"slack-radar-mcp": "dist/mcp/index.js",
|
|
9
10
|
"slack-radar-web": "dist/web/bin.js"
|
|
10
11
|
},
|