@slack/radar-mcp 1.1.1 → 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 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: "Port forwarding failed. Ensure a device is connected.",
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.0" }, { capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS });
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
  }));
@@ -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;
@@ -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(`adb forward tcp:${RADAR_PORT} localabstract:${SOCKET_NAME}`, {
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(`adb shell am broadcast --user ${userId} -a ${ENABLE_ACTION} --ei timeout_minutes ${timeout}`, { timeout: 5000 });
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(`adb shell screencap -p ${devicePath}`, { timeout: 10000 });
79
- execSync(`adb pull ${devicePath} "${tmpFile}"`, { timeout: 10000 });
80
- execSync(`adb shell rm ${devicePath}`, { timeout: 5000 });
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(`adb shell screenrecord --time-limit ${duration} --size 720x1280 ${devicePath}`, { timeout: (duration + 5) * 1000 });
96
- execSync(`adb pull ${devicePath} "${tmpFile}"`, { timeout: 10000 });
97
- execSync(`adb shell rm ${devicePath}`, { timeout: 5000 });
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
- return execSync(`adb shell "dumpsys activity activities | grep -A 5 'com.Slack.internal'"`, { timeout: 10000, encoding: "utf-8" }).trim();
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
- let cmd = `adb logcat -d | grep -F "${tag}" | tail -${limit}`;
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 = `adb logcat -d | grep -F "${tag}" | grep -i "${grep.replace(/"/g, '\\"')}" | tail -${limit}`;
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(`adb shell "ps -A -o USER,NAME 2>/dev/null | grep Slack | grep debug"`, { timeout: 5000, encoding: "utf-8" }).trim();
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(`adb shell am broadcast --user ${userId} -a ${ENABLE_ACTION} --ei timeout_minutes ${timeout}`, { timeout: 5000 });
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(`adb shell "ps -A -o USER,NAME 2>/dev/null | grep Slack | grep debug"`, { timeout: 5000, encoding: "utf-8" }).trim();
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
- <select id="profile-select" style="display: none; margin-right: 4px; font-size: 12px; padding: 2px 4px; background: #2a2a2a; color: #ccc; border: 1px solid #444; border-radius: 3px;"></select>
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 profileSelect = document.getElementById("profile-select");
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 selectedUser = profileSelect.value;
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
- profileSelect.innerHTML = "";
334
- if (profiles.length > 1) {
335
- for (const p of profiles) {
336
- const opt = document.createElement("option");
337
- opt.value = p.userId;
338
- opt.textContent = p.label;
339
- profileSelect.appendChild(opt);
340
- }
341
- profileSelect.style.display = "";
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
- profileSelect.style.display = "none";
348
+ profileLabel.style.display = "none";
344
349
  }
345
350
  } catch {
346
- profileSelect.style.display = "none";
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
- fetchProfiles();
354
- } else {
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 = state.buffers[type];
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
- const buf = state.buffers[type];
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
- document.getElementById("network-count").textContent = state.buffers.network.length;
415
- document.getElementById("rtm-count").textContent = state.buffers.rtm.length;
416
- document.getElementById("clog-count").textContent = state.buffers.clog.length;
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
- connectStream();
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>
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "@slack/radar-mcp",
3
- "version": "1.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",