@slack/radar-mcp 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Slack Technologies, LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,148 @@
1
+ # slack-radar-mcp
2
+
3
+ Platform-agnostic MCP server for on-device debugging of mobile apps.
4
+
5
+ Gives Claude Code direct access to network calls, RTM events, analytics (clogs), screenshots, logs, and more from a running debug build. Ships with Android support (ADB); the `DeviceTransport` interface makes adding iOS or other platforms straightforward.
6
+
7
+ > **For Slack engineers.** The npm package is published publicly as a distribution convenience, but the source repository, issue tracker, and support live on Slack's internal GitHub Enterprise. External users can install the package but should not expect support or source access. The tool only functions against a Slack debug build, which is not distributed externally.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ git clone slack-github.com/slack/slack-radar-mcp
13
+ cd slack-radar-mcp
14
+ npm install
15
+ npm run build
16
+ ```
17
+
18
+ Register the server with Claude Code:
19
+
20
+ ```bash
21
+ slack claude mcp add -s user slack-radar-mcp -- node /path/to/slack-radar-mcp/dist/index.js
22
+ ```
23
+
24
+ Restart your Claude Code session for the tools to appear.
25
+
26
+ ## Prerequisites
27
+
28
+ * Node.js 18+
29
+ * ADB with a single connected device (USB or wireless debug)
30
+ * A debug build of Slack installed on the device (must include the `slack-radar` modules)
31
+
32
+ ## Session Management
33
+
34
+ Radar starts **disabled** each session. All tools except `ping` return an error until enabled.
35
+
36
+ | Command | Effect |
37
+ |---|---|
38
+ | `/radar-enable` | Activates ADB forwarding, device broadcast, SSE stream. Tools become available. |
39
+ | `/radar-disable` | Tears down stream, clears local buffers. Tools return "not active" error. |
40
+
41
+ **Auto-enable**: When you ask Claude an obvious device question (clogs, network calls, screenshots, app state, RTM events, logs), Claude invokes `/radar-enable` automatically before calling the relevant tool.
42
+
43
+ **Auto-disable**: When the device-side radar shuts down after idle (default 5 min), the next tool call failure flips the session to disabled. Claude will tell you and offer to re-enable.
44
+
45
+ ## Tools
46
+
47
+ | Tool | Description |
48
+ |---|---|
49
+ | `ping` | Health check. Returns uptime, buffer sizes, remaining session time, stream status. Use `timeout_minutes` to extend sessions. |
50
+ | `get_recent_network_calls` | HTTP call summaries: method, URL, status code, duration. Filterable by URL substring and status code. |
51
+ | `get_network_call_detail` | Full request/response headers and bodies for a specific call (gzip/brotli decompressed). |
52
+ | `get_recent_rtm_events` | RTM event summaries: type, direction, channel, timestamp. Filterable by type, direction, channel. Uses subtypes (e.g. `message_changed`, not `message`). |
53
+ | `get_rtm_event_detail` | Full RTM event JSON payload for a specific event. |
54
+ | `get_recent_clogs` | Analytics event summaries: name and timestamp. Filterable by event name substring. Persists from app start. |
55
+ | `get_clog_detail` | Full clog JSON payload for a specific event. |
56
+ | `wait_for_events` | Block until matching events arrive via SSE stream. Idle-timeout based, not polling. |
57
+ | `search` | Full-text search across network, RTM, and clog buffers. |
58
+ | `get_app_state` | Current Activity/Fragment state via `dumpsys`. Shows visible screen, parameters, tab state. |
59
+ | `get_recent_logs` | Logcat entries filtered by tag. For Timber debug logs (`FKDebug`, `SlackRadar`, etc.). |
60
+ | `screenshot` | Capture device screen as PNG. 720px default, `full_res=true` for full resolution. |
61
+ | `record_screen` | Record short screen video (MP4). Default 5s, max 30s. |
62
+ | `clear_buffers` | Clear all captured network calls and RTM events from device and local buffers. |
63
+ | `_radar_session_enable` | Enable radar for the session. Called by `/radar-enable` skill. |
64
+ | `_radar_session_disable` | Disable radar for the session. Called by `/radar-disable` skill. |
65
+
66
+ ## Architecture
67
+
68
+ ```
69
+ Mobile debug build (device)
70
+ +-- Local socket (platform-specific)
71
+ +-- HTTP endpoints + SSE streaming
72
+ |
73
+ | port forwarding (adb forward / iproxy / etc.)
74
+ |
75
+ MCP server (Node.js, runs on developer machine)
76
+ +-- DeviceTransport (AndroidTransport, future: IosTransport)
77
+ +-- Stdio transport -> Claude Code
78
+ +-- 16 tools: ping, network, rtm, clogs, search, ...
79
+ ```
80
+
81
+ All device-specific logic lives behind the `DeviceTransport` interface (`src/transport.ts`). The MCP server, tools, SSE streaming, and session management are fully platform-agnostic.
82
+
83
+ **Adding a new platform**: implement `DeviceTransport` in `src/<platform>.ts`, add a case to `createTransport()` in `src/index.ts`. No other files change.
84
+
85
+ ## Security
86
+
87
+ * **No TCP port on device.** Uses platform-specific local sockets, only reachable via port forwarding. Requires physical USB or explicit debug pairing.
88
+ * **Debug builds only.** Release builds bind a no-op implementation. Server code is eliminated at compile time.
89
+ * **Lazy activation.** Dormant until explicitly triggered.
90
+ * **Auto-shutdown.** Device-side server shuts down after idle timeout (default 5 minutes). Zero cost when unused.
91
+
92
+ ## Data Capture
93
+
94
+ What gets captured depends on the in-app server implementation. The MCP server is agnostic to the data source. The Android implementation captures:
95
+
96
+ * **Network calls**: OkHttp interceptor. Ring buffer: 200 calls.
97
+ * **RTM events**: WebSocket listener. Ring buffer: 500 events.
98
+ * **Clogs**: Analytics interceptor. Ring buffer: 500 events. Persists across radar activation cycles.
99
+
100
+ All data is in-memory only. No disk writes.
101
+
102
+ ### What gets captured, in detail
103
+
104
+ This tool captures unredacted network traffic from your debug Slack build, including:
105
+
106
+ * URL query parameters (tokens can appear here)
107
+ * `Authorization` headers and cookies
108
+ * `Set-Cookie` response headers (session tokens)
109
+ * Request and response bodies, up to 64 KB per body, for JSON, form, text, and XML content types
110
+ * RTM WebSocket payloads (every sent and received message)
111
+ * Analytics (clog) events serialized as JSON
112
+ * Screenshots and short screen recordings on demand
113
+ * Logcat entries filtered by tag
114
+
115
+ Auth tokens and session cookies will appear in captured data. Captured data flows to your MCP client (Claude Code), which may send relevant portions to the configured AI provider per its own data handling policy.
116
+
117
+ ### Use against workspaces you are signed into as yourself
118
+
119
+ The tool is intended for debugging the Slack Android app against your own sessions. Do not share captured data (screenshots, log exports, tool transcripts) externally without first redacting auth material. Treat anything radar captures with the same care you would treat a `curl` transcript of an authenticated API call.
120
+
121
+ ## Local Development
122
+
123
+ ```bash
124
+ npm install
125
+ npm run build # Compile TypeScript to dist/
126
+ npm start # Run the server (stdio transport)
127
+ npm run watch # TypeScript compiler in watch mode
128
+ npm test # Unit tests
129
+ npm run test:smoke # Smoke tests
130
+ npm run test:all # Both
131
+ ```
132
+
133
+ ## Troubleshooting
134
+
135
+ **"ADB port forwarding failed"**
136
+ Ensure a device is connected (`adb devices` should show exactly one device). If using wireless debug, confirm the pairing is active.
137
+
138
+ **"Radar not responding"**
139
+ The device-side server may have auto-shut down. Use `/radar-enable` to restart it.
140
+
141
+ **"Session disabled"**
142
+ Two consecutive connection failures disable the session. Use `/radar-enable` to reconnect.
143
+
144
+ **Work profile devices**
145
+ The MCP server auto-detects the Android user ID via `ps` and targets the broadcast accordingly. No manual `--user` flag needed.
146
+
147
+ **Tools not appearing after setup**
148
+ Restart your Claude Code session. MCP servers are registered at session start.
@@ -0,0 +1,25 @@
1
+ import type { DeviceTransport } from "./transport.js";
2
+ /**
3
+ * Parse the output of `ps -A -o USER,NAME` to extract the Android user ID
4
+ * for the debug build process. When multiple profiles are running, prefers
5
+ * the work profile (user ID > 0) since that's the typical debugging target.
6
+ * Returns "0" for primary user, "10" for work profile, etc.
7
+ */
8
+ export declare function parseUserFromPsOutput(psOutput: string): string;
9
+ export declare class AndroidTransport implements DeviceTransport {
10
+ readonly platform = "android";
11
+ private portForwarded;
12
+ private detectedUserId;
13
+ private currentTimeoutMinutes;
14
+ private radarEnabled;
15
+ ensureForward(): boolean;
16
+ activate(timeoutMinutes: number): void;
17
+ resetState(): void;
18
+ isForwarded(): boolean;
19
+ getTimeoutMinutes(): number;
20
+ screenshot(fullRes: boolean): string;
21
+ recordScreen(durationS: number): string;
22
+ getAppState(): string;
23
+ getRecentLogs(tag: string, limit: number, grep?: string): string[];
24
+ private detectDebugBuildUser;
25
+ }
@@ -0,0 +1,127 @@
1
+ import { execSync } from "child_process";
2
+ import os from "os";
3
+ import path from "path";
4
+ const RADAR_PORT = 8099;
5
+ const SOCKET_NAME = "slack-radar";
6
+ const ENABLE_ACTION = "slack.debug.ENABLE_RADAR";
7
+ /**
8
+ * Parse the output of `ps -A -o USER,NAME` to extract the Android user ID
9
+ * for the debug build process. When multiple profiles are running, prefers
10
+ * the work profile (user ID > 0) since that's the typical debugging target.
11
+ * Returns "0" for primary user, "10" for work profile, etc.
12
+ */
13
+ export function parseUserFromPsOutput(psOutput) {
14
+ if (!psOutput || !psOutput.trim())
15
+ return "0";
16
+ const lines = psOutput.trim().split("\n");
17
+ // Collect all user IDs from matching lines
18
+ const userIds = [];
19
+ for (const line of lines) {
20
+ const match = line.match(/u(\d+)_/);
21
+ if (match)
22
+ userIds.push(match[1]);
23
+ }
24
+ if (userIds.length === 0)
25
+ return "0";
26
+ // Prefer work profile (non-zero user ID) when multiple profiles are running
27
+ const workProfile = userIds.find((id) => id !== "0");
28
+ return workProfile ?? userIds[0];
29
+ }
30
+ export class AndroidTransport {
31
+ platform = "android";
32
+ portForwarded = false;
33
+ detectedUserId = null;
34
+ currentTimeoutMinutes = 5;
35
+ radarEnabled = false;
36
+ ensureForward() {
37
+ if (this.portForwarded)
38
+ return true;
39
+ try {
40
+ execSync(`adb forward tcp:${RADAR_PORT} localabstract:${SOCKET_NAME}`, {
41
+ timeout: 5000,
42
+ });
43
+ this.portForwarded = true;
44
+ return true;
45
+ }
46
+ catch {
47
+ return false;
48
+ }
49
+ }
50
+ activate(timeoutMinutes) {
51
+ const timeout = timeoutMinutes || this.currentTimeoutMinutes;
52
+ if (this.radarEnabled && timeout === this.currentTimeoutMinutes)
53
+ return;
54
+ try {
55
+ const userId = this.detectDebugBuildUser();
56
+ execSync(`adb shell am broadcast --user ${userId} -a ${ENABLE_ACTION} --ei timeout_minutes ${timeout}`, { timeout: 5000 });
57
+ this.currentTimeoutMinutes = timeout;
58
+ this.radarEnabled = true;
59
+ }
60
+ catch {
61
+ // Broadcast failed — device may not be connected
62
+ }
63
+ }
64
+ resetState() {
65
+ this.portForwarded = false;
66
+ this.detectedUserId = null;
67
+ this.radarEnabled = false;
68
+ }
69
+ isForwarded() {
70
+ return this.portForwarded;
71
+ }
72
+ getTimeoutMinutes() {
73
+ return this.currentTimeoutMinutes;
74
+ }
75
+ screenshot(fullRes) {
76
+ const tmpFile = path.join(os.tmpdir(), `slack-radar-screenshot-${Date.now()}.png`);
77
+ 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 });
81
+ if (!fullRes) {
82
+ try {
83
+ execSync(`sips -Z 720 "${tmpFile}" 2>/dev/null`, { timeout: 5000 });
84
+ }
85
+ catch {
86
+ // Resize failed; keep full res
87
+ }
88
+ }
89
+ return tmpFile;
90
+ }
91
+ recordScreen(durationS) {
92
+ const duration = Math.min(Math.max(durationS, 1), 30);
93
+ const tmpFile = path.join(os.tmpdir(), `slack-radar-recording-${Date.now()}.mp4`);
94
+ 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 });
98
+ return tmpFile;
99
+ }
100
+ getAppState() {
101
+ return execSync(`adb shell "dumpsys activity activities | grep -A 5 'com.Slack.internal'"`, { timeout: 10000, encoding: "utf-8" }).trim();
102
+ }
103
+ getRecentLogs(tag, limit, grep) {
104
+ let cmd = `adb logcat -d | grep -F "${tag}" | tail -${limit}`;
105
+ if (grep) {
106
+ cmd = `adb logcat -d | grep -F "${tag}" | grep -i "${grep.replace(/"/g, '\\"')}" | tail -${limit}`;
107
+ }
108
+ const output = execSync(cmd, {
109
+ timeout: 10000,
110
+ shell: "/bin/zsh",
111
+ encoding: "utf-8",
112
+ });
113
+ return output.trim().split("\n").filter(Boolean);
114
+ }
115
+ detectDebugBuildUser() {
116
+ if (this.detectedUserId !== null)
117
+ return this.detectedUserId;
118
+ try {
119
+ const ps = execSync(`adb shell "ps -A -o USER,NAME 2>/dev/null | grep Slack | grep debug"`, { timeout: 5000, encoding: "utf-8" }).trim();
120
+ this.detectedUserId = parseUserFromPsOutput(ps);
121
+ }
122
+ catch {
123
+ this.detectedUserId = "0";
124
+ }
125
+ return this.detectedUserId;
126
+ }
127
+ }
@@ -0,0 +1,6 @@
1
+ /** Port used to reach the on-device radar HTTP server via forwarding. */
2
+ export declare const RADAR_PORT = 8099;
3
+ /** Host address for the forwarded connection. */
4
+ export declare const RADAR_HOST = "127.0.0.1";
5
+ /** Max events to retain in each local SSE buffer. */
6
+ export declare const MAX_LOCAL_BUFFER = 500;
@@ -0,0 +1,6 @@
1
+ /** Port used to reach the on-device radar HTTP server via forwarding. */
2
+ export const RADAR_PORT = 8099;
3
+ /** Host address for the forwarded connection. */
4
+ export const RADAR_HOST = "127.0.0.1";
5
+ /** Max events to retain in each local SSE buffer. */
6
+ export const MAX_LOCAL_BUFFER = 500;
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,341 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
+ import fs from "fs";
6
+ import http from "http";
7
+ import { AndroidTransport } from "./android.js";
8
+ import { RADAR_HOST, RADAR_PORT } from "./constants.js";
9
+ import { connectStream, destroyStream, waitForEvents, clearLocalBuffers, isStreamConnected, localNetworkBuffer, localRtmBuffer, setTransport, } from "./stream.js";
10
+ import { TOOL_DEFINITIONS, SERVER_INSTRUCTIONS } from "./tools.js";
11
+ // --- Platform selection ---
12
+ function createTransport() {
13
+ const platformArg = process.argv.find((a) => a.startsWith("--platform="));
14
+ const platform = platformArg?.split("=")[1] ?? "android";
15
+ switch (platform) {
16
+ case "android":
17
+ return new AndroidTransport();
18
+ default:
19
+ throw new Error(`Unsupported platform: ${platform}. Supported: android`);
20
+ }
21
+ }
22
+ const device = createTransport();
23
+ setTransport(device);
24
+ let sessionEnabled = false;
25
+ function radarRequest(requestPath) {
26
+ return new Promise((resolve, reject) => {
27
+ const req = http.get({
28
+ host: RADAR_HOST,
29
+ port: RADAR_PORT,
30
+ path: requestPath,
31
+ timeout: 10000,
32
+ }, (res) => {
33
+ let data = "";
34
+ res.on("data", (chunk) => {
35
+ data += chunk;
36
+ });
37
+ res.on("end", () => {
38
+ try {
39
+ resolve(JSON.parse(data));
40
+ }
41
+ catch {
42
+ resolve({ raw: data });
43
+ }
44
+ });
45
+ });
46
+ req.on("error", reject);
47
+ req.on("timeout", () => {
48
+ req.destroy();
49
+ reject(new Error("Request timed out"));
50
+ });
51
+ });
52
+ }
53
+ async function callRadar(requestPath) {
54
+ if (!device.ensureForward()) {
55
+ return {
56
+ error: "Port forwarding failed. Ensure a device is connected and the debug build is running.",
57
+ };
58
+ }
59
+ device.activate(device.getTimeoutMinutes());
60
+ connectStream();
61
+ try {
62
+ return await radarRequest(requestPath);
63
+ }
64
+ catch {
65
+ device.resetState();
66
+ destroyStream();
67
+ if (!device.ensureForward()) {
68
+ sessionEnabled = false;
69
+ return {
70
+ error: "Device not connected or radar not running. Session disabled. Use /radar-enable to reconnect.",
71
+ };
72
+ }
73
+ device.activate(device.getTimeoutMinutes());
74
+ connectStream();
75
+ try {
76
+ return await radarRequest(requestPath);
77
+ }
78
+ catch (e2) {
79
+ sessionEnabled = false;
80
+ return {
81
+ error: `Radar not responding: ${e2.message}. Session disabled. Use /radar-enable to reconnect.`,
82
+ };
83
+ }
84
+ }
85
+ }
86
+ function textResult(data, isError = false) {
87
+ return {
88
+ content: [
89
+ {
90
+ type: "text",
91
+ text: typeof data === "string" ? data : JSON.stringify(data, null, 2),
92
+ },
93
+ ],
94
+ ...(isError ? { isError: true } : {}),
95
+ };
96
+ }
97
+ // --- Tool handlers ---
98
+ async function handleSessionEnable(args) {
99
+ const timeout = args?.timeout_minutes || 5;
100
+ sessionEnabled = true;
101
+ const forwarded = device.ensureForward();
102
+ if (!forwarded) {
103
+ sessionEnabled = false;
104
+ return textResult({
105
+ error: "Port forwarding failed. Ensure a device is connected.",
106
+ enabled: false,
107
+ platform: device.platform,
108
+ }, true);
109
+ }
110
+ device.activate(timeout);
111
+ connectStream();
112
+ await new Promise((r) => setTimeout(r, 500));
113
+ try {
114
+ const status = await radarRequest("/api/ping");
115
+ return textResult({
116
+ enabled: true,
117
+ platform: device.platform,
118
+ stream_connected: isStreamConnected(),
119
+ ...status,
120
+ });
121
+ }
122
+ catch {
123
+ return textResult({
124
+ enabled: true,
125
+ platform: device.platform,
126
+ stream_connected: isStreamConnected(),
127
+ ping: "failed",
128
+ note: "Radar activated but ping failed. Device may need a moment.",
129
+ });
130
+ }
131
+ }
132
+ function handleSessionDisable() {
133
+ sessionEnabled = false;
134
+ destroyStream();
135
+ clearLocalBuffers();
136
+ return textResult({
137
+ enabled: false,
138
+ note: "Radar disabled. Use /radar-enable to reconnect.",
139
+ });
140
+ }
141
+ async function handleWaitForEvents(args) {
142
+ if (!isStreamConnected()) {
143
+ if (!device.ensureForward()) {
144
+ return textResult({ error: "Not connected. Ensure device is connected and radar is enabled." }, true);
145
+ }
146
+ device.activate(device.getTimeoutMinutes());
147
+ connectStream();
148
+ await new Promise((r) => setTimeout(r, 1000));
149
+ }
150
+ const events = await waitForEvents({
151
+ event_type: args?.event_type,
152
+ channel: args?.channel,
153
+ source: args?.source,
154
+ timeout_ms: args?.timeout_ms,
155
+ idle_timeout_ms: args?.idle_timeout_ms,
156
+ });
157
+ return textResult({
158
+ event_count: events.length,
159
+ events,
160
+ stream_connected: isStreamConnected(),
161
+ });
162
+ }
163
+ function handleGetAppState() {
164
+ try {
165
+ const output = device.getAppState();
166
+ return textResult(output);
167
+ }
168
+ catch (e) {
169
+ return textResult({ error: `App state query failed: ${e.message}` }, true);
170
+ }
171
+ }
172
+ function handleGetRecentLogs(args) {
173
+ try {
174
+ const lines = device.getRecentLogs(args.tag, args.limit || 50, args.grep);
175
+ return textResult({ tag: args.tag, lines, count: lines.length });
176
+ }
177
+ catch (e) {
178
+ return textResult({ error: `Log query failed: ${e.message}`, lines: [] }, true);
179
+ }
180
+ }
181
+ function handleScreenshot(args) {
182
+ try {
183
+ const fullRes = args?.full_res === true;
184
+ const tmpFile = device.screenshot(fullRes);
185
+ const stats = fs.statSync(tmpFile);
186
+ return textResult({
187
+ file: tmpFile,
188
+ size_bytes: stats.size,
189
+ full_res: fullRes,
190
+ hint: "Use the Read tool to view this image.",
191
+ });
192
+ }
193
+ catch (e) {
194
+ return textResult({ error: `Screenshot failed: ${e.message}` }, true);
195
+ }
196
+ }
197
+ function handleRecordScreen(args) {
198
+ try {
199
+ const duration = Math.min(Math.max(args?.duration_s || 5, 1), 30);
200
+ const tmpFile = device.recordScreen(duration);
201
+ const stats = fs.statSync(tmpFile);
202
+ return textResult({
203
+ file: tmpFile,
204
+ size_bytes: stats.size,
205
+ duration_s: duration,
206
+ });
207
+ }
208
+ catch (e) {
209
+ return textResult({ error: `Recording failed: ${e.message}` }, true);
210
+ }
211
+ }
212
+ async function handleClearBuffers() {
213
+ clearLocalBuffers();
214
+ const result = await callRadar("/api/clear");
215
+ return textResult(result, !!result.error);
216
+ }
217
+ async function handlePing(args) {
218
+ if (args?.timeout_minutes) {
219
+ device.activate(args.timeout_minutes);
220
+ }
221
+ const result = await callRadar("/api/ping");
222
+ if (!result.error) {
223
+ result.stream_connected = isStreamConnected();
224
+ result.local_network_buffer = localNetworkBuffer.length;
225
+ result.local_rtm_buffer = localRtmBuffer.length;
226
+ result.timeout_minutes = device.getTimeoutMinutes();
227
+ result.platform = device.platform;
228
+ }
229
+ return textResult(result, !!result.error);
230
+ }
231
+ function buildApiPath(name, args) {
232
+ switch (name) {
233
+ case "get_recent_network_calls": {
234
+ const a = args;
235
+ const params = new URLSearchParams();
236
+ if (a?.limit)
237
+ params.set("limit", String(a.limit));
238
+ if (a?.url_filter)
239
+ params.set("url_filter", a.url_filter);
240
+ if (a?.status_code)
241
+ params.set("status_code", String(a.status_code));
242
+ const qs = params.toString();
243
+ return `/api/network${qs ? "?" + qs : ""}`;
244
+ }
245
+ case "get_network_call_detail": {
246
+ const a = args;
247
+ return `/api/network/${a.id}`;
248
+ }
249
+ case "get_recent_rtm_events": {
250
+ const a = args;
251
+ const params = new URLSearchParams();
252
+ if (a?.limit)
253
+ params.set("limit", String(a.limit));
254
+ if (a?.event_type)
255
+ params.set("event_type", a.event_type);
256
+ if (a?.direction)
257
+ params.set("direction", a.direction);
258
+ if (a?.channel)
259
+ params.set("channel", a.channel);
260
+ const qs = params.toString();
261
+ return `/api/rtm${qs ? "?" + qs : ""}`;
262
+ }
263
+ case "get_rtm_event_detail": {
264
+ const a = args;
265
+ return `/api/rtm/${a.id}`;
266
+ }
267
+ case "get_recent_clogs": {
268
+ const a = args;
269
+ const params = new URLSearchParams();
270
+ if (a?.limit)
271
+ params.set("limit", String(a.limit));
272
+ if (a?.name)
273
+ params.set("name", a.name);
274
+ const qs = params.toString();
275
+ return `/api/clogs${qs ? "?" + qs : ""}`;
276
+ }
277
+ case "get_clog_detail": {
278
+ const a = args;
279
+ return `/api/clogs/${a.id}`;
280
+ }
281
+ case "search": {
282
+ const a = args;
283
+ const params = new URLSearchParams();
284
+ params.set("q", a.query);
285
+ if (a?.limit)
286
+ params.set("limit", String(a.limit));
287
+ return `/api/search?${params.toString()}`;
288
+ }
289
+ default:
290
+ return null;
291
+ }
292
+ }
293
+ // --- MCP Server ---
294
+ const server = new Server({ name: "slack-radar", version: "1.1.0" }, { capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS });
295
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
296
+ tools: TOOL_DEFINITIONS,
297
+ }));
298
+ server.setRequestHandler(CallToolRequestSchema, async (request, _extra) => {
299
+ const { name, arguments: args } = request.params;
300
+ const toolArgs = (args ?? {});
301
+ if (name === "_radar_session_enable") {
302
+ return handleSessionEnable(toolArgs);
303
+ }
304
+ if (name === "_radar_session_disable") {
305
+ return handleSessionDisable();
306
+ }
307
+ if (!sessionEnabled && name !== "ping") {
308
+ return textResult({
309
+ error: "Radar is not active. Use /radar-enable to connect, or ask about the device to auto-enable.",
310
+ }, true);
311
+ }
312
+ if (name === "wait_for_events") {
313
+ return handleWaitForEvents(toolArgs);
314
+ }
315
+ if (name === "get_app_state") {
316
+ return handleGetAppState();
317
+ }
318
+ if (name === "get_recent_logs") {
319
+ return handleGetRecentLogs(toolArgs);
320
+ }
321
+ if (name === "screenshot") {
322
+ return handleScreenshot(toolArgs);
323
+ }
324
+ if (name === "record_screen") {
325
+ return handleRecordScreen(toolArgs);
326
+ }
327
+ if (name === "clear_buffers") {
328
+ return handleClearBuffers();
329
+ }
330
+ if (name === "ping") {
331
+ return handlePing(toolArgs);
332
+ }
333
+ const apiPath = buildApiPath(name, toolArgs);
334
+ if (apiPath === null) {
335
+ return textResult(`Unknown tool: ${name}`, true);
336
+ }
337
+ const result = await callRadar(apiPath);
338
+ return textResult(result, !!result.error);
339
+ });
340
+ const transport = new StdioServerTransport();
341
+ await server.connect(transport);
@@ -0,0 +1,31 @@
1
+ import http from "http";
2
+ import type { DeviceTransport } from "./transport.js";
3
+ /** Set the transport used to check forwarding state. Call once at startup. */
4
+ export declare function setTransport(t: DeviceTransport): void;
5
+ export interface RadarEvent {
6
+ [key: string]: unknown;
7
+ }
8
+ export interface ParsedSSE {
9
+ type: "network" | "rtm";
10
+ event: RadarEvent;
11
+ }
12
+ export declare const localNetworkBuffer: RadarEvent[];
13
+ export declare const localRtmBuffer: RadarEvent[];
14
+ export declare function isStreamConnected(): boolean;
15
+ export declare function clearLocalBuffers(): void;
16
+ export declare function getStreamConnection(): http.ClientRequest | null;
17
+ export declare function destroyStream(): void;
18
+ /**
19
+ * Push an event into the appropriate local buffer, enforcing MAX_LOCAL_BUFFER.
20
+ * Exported for testability.
21
+ */
22
+ export declare function bufferEvent(parsed: ParsedSSE): void;
23
+ export interface WaitForEventsArgs {
24
+ event_type?: string;
25
+ channel?: string;
26
+ source?: string;
27
+ timeout_ms?: number;
28
+ idle_timeout_ms?: number;
29
+ }
30
+ export declare function waitForEvents({ event_type, channel, source, timeout_ms, idle_timeout_ms, }: WaitForEventsArgs): Promise<RadarEvent[]>;
31
+ export declare function connectStream(): void;
package/dist/stream.js ADDED
@@ -0,0 +1,137 @@
1
+ import http from "http";
2
+ import { RADAR_HOST, RADAR_PORT, MAX_LOCAL_BUFFER } from "./constants.js";
3
+ let transport = null;
4
+ /** Set the transport used to check forwarding state. Call once at startup. */
5
+ export function setTransport(t) {
6
+ transport = t;
7
+ }
8
+ export const localNetworkBuffer = [];
9
+ export const localRtmBuffer = [];
10
+ let streamConnection = null;
11
+ let streamConnected = false;
12
+ const eventWaiters = [];
13
+ export function isStreamConnected() {
14
+ return streamConnected;
15
+ }
16
+ export function clearLocalBuffers() {
17
+ localNetworkBuffer.length = 0;
18
+ localRtmBuffer.length = 0;
19
+ }
20
+ export function getStreamConnection() {
21
+ return streamConnection;
22
+ }
23
+ export function destroyStream() {
24
+ if (streamConnection) {
25
+ streamConnection.destroy();
26
+ streamConnection = null;
27
+ streamConnected = false;
28
+ }
29
+ }
30
+ /**
31
+ * Push an event into the appropriate local buffer, enforcing MAX_LOCAL_BUFFER.
32
+ * Exported for testability.
33
+ */
34
+ export function bufferEvent(parsed) {
35
+ if (parsed.type === "network") {
36
+ localNetworkBuffer.push(parsed.event);
37
+ if (localNetworkBuffer.length > MAX_LOCAL_BUFFER)
38
+ localNetworkBuffer.shift();
39
+ }
40
+ else if (parsed.type === "rtm") {
41
+ localRtmBuffer.push(parsed.event);
42
+ if (localRtmBuffer.length > MAX_LOCAL_BUFFER)
43
+ localRtmBuffer.shift();
44
+ }
45
+ notifyWaiters(parsed);
46
+ }
47
+ function notifyWaiters(parsed) {
48
+ for (const waiter of eventWaiters) {
49
+ const e = parsed.event;
50
+ const eventType = e.event_type;
51
+ const channel = e.channel;
52
+ const matchesType = !waiter.eventType ||
53
+ (eventType && eventType.toLowerCase() === waiter.eventType.toLowerCase());
54
+ const matchesChannel = !waiter.channel ||
55
+ (channel && channel.toLowerCase() === waiter.channel.toLowerCase());
56
+ const matchesSource = !waiter.source || parsed.type === waiter.source;
57
+ if (matchesType && matchesChannel && matchesSource) {
58
+ waiter.matches.push(e);
59
+ waiter.lastMatchTime = Date.now();
60
+ if (waiter.idleTimer)
61
+ clearTimeout(waiter.idleTimer);
62
+ waiter.idleTimer = setTimeout(() => {
63
+ waiter.resolve(waiter.matches);
64
+ }, waiter.idleTimeoutMs);
65
+ }
66
+ }
67
+ }
68
+ export function waitForEvents({ event_type, channel, source, timeout_ms = 30000, idle_timeout_ms = 3000, }) {
69
+ return new Promise((resolve) => {
70
+ const waiter = {
71
+ eventType: event_type,
72
+ channel,
73
+ source,
74
+ idleTimeoutMs: idle_timeout_ms,
75
+ matches: [],
76
+ lastMatchTime: null,
77
+ idleTimer: null,
78
+ resolve: (result) => {
79
+ cleanup();
80
+ resolve(result);
81
+ },
82
+ };
83
+ function cleanup() {
84
+ const idx = eventWaiters.indexOf(waiter);
85
+ if (idx >= 0)
86
+ eventWaiters.splice(idx, 1);
87
+ if (waiter.idleTimer)
88
+ clearTimeout(waiter.idleTimer);
89
+ if (maxTimer)
90
+ clearTimeout(maxTimer);
91
+ }
92
+ eventWaiters.push(waiter);
93
+ const maxTimer = setTimeout(() => {
94
+ waiter.resolve(waiter.matches);
95
+ }, timeout_ms);
96
+ });
97
+ }
98
+ export function connectStream() {
99
+ if (streamConnection || !transport?.isForwarded())
100
+ return;
101
+ const req = http.get({ host: RADAR_HOST, port: RADAR_PORT, path: "/api/stream", timeout: 0 }, (res) => {
102
+ streamConnected = true;
103
+ let partial = "";
104
+ res.on("data", (chunk) => {
105
+ partial += chunk.toString();
106
+ const lines = partial.split("\n");
107
+ partial = lines.pop();
108
+ for (const line of lines) {
109
+ if (!line.startsWith("data: "))
110
+ continue;
111
+ try {
112
+ const parsed = JSON.parse(line.slice(6));
113
+ bufferEvent(parsed);
114
+ }
115
+ catch {
116
+ // Malformed SSE line — skip
117
+ }
118
+ }
119
+ });
120
+ res.on("end", () => {
121
+ streamConnection = null;
122
+ streamConnected = false;
123
+ setTimeout(connectStream, 2000);
124
+ });
125
+ res.on("error", () => {
126
+ streamConnection = null;
127
+ streamConnected = false;
128
+ setTimeout(connectStream, 2000);
129
+ });
130
+ });
131
+ req.on("error", () => {
132
+ streamConnection = null;
133
+ streamConnected = false;
134
+ setTimeout(connectStream, 2000);
135
+ });
136
+ streamConnection = req;
137
+ }
@@ -0,0 +1,22 @@
1
+ export declare const SERVER_INSTRUCTIONS: string;
2
+ export interface ToolDefinition {
3
+ name: string;
4
+ description: string;
5
+ inputSchema: {
6
+ type: string;
7
+ properties: Record<string, unknown>;
8
+ required?: string[];
9
+ };
10
+ }
11
+ export declare const TOOL_DEFINITIONS: ToolDefinition[];
12
+ /**
13
+ * Check whether a tool call should be blocked by the session gate.
14
+ * Returns an error response object if blocked, or null if allowed.
15
+ */
16
+ export declare function sessionGate(toolName: string, sessionEnabled: boolean): {
17
+ content: Array<{
18
+ type: string;
19
+ text: string;
20
+ }>;
21
+ isError: boolean;
22
+ } | null;
package/dist/tools.js ADDED
@@ -0,0 +1,231 @@
1
+ export const SERVER_INSTRUCTIONS = [
2
+ "Slack Radar provides on-device debugging tools for the Slack Android app via ADB.",
3
+ "",
4
+ "SESSION STATE:",
5
+ "Radar starts DISABLED each session. All tools except ping return an error until enabled.",
6
+ "Use /radar-enable to activate, /radar-disable to deactivate.",
7
+ "",
8
+ "AUTO-ENABLE: When the user asks an obvious device question, invoke the Skill tool with",
9
+ 'skill="radar-enable" FIRST, then proceed with the radar tool call. No need to ask.',
10
+ "Obvious device questions include: clogs, network calls, RTM events, screenshots,",
11
+ "app state, device logs, or any reference to what is happening on the connected device.",
12
+ "",
13
+ "TIMEOUT: The device-side radar auto-shuts down after idle. On tool call failure,",
14
+ "the session flips to disabled. Tell the user and offer to re-enable.",
15
+ "",
16
+ "When to use:",
17
+ '- "what am I looking at" / "what screen is this" -> screenshot',
18
+ '- "what just happened" / "what\'s going on" -> get_recent_network_calls + get_recent_rtm_events',
19
+ '- "why isn\'t this loading" / "this is broken" -> screenshot + get_recent_network_calls(status_code=400 or 500)',
20
+ '- "watch what happens when I..." -> clear_buffers then wait_for_events (DO NOT filter unless the user specifies a type)',
21
+ '- "did that clog fire" / "check analytics" -> get_recent_clogs(name="...")',
22
+ '- "what API call was that" -> get_recent_network_calls then get_network_call_detail',
23
+ '- "did I get X" / "did X happen" -> search(query="X") across ALL buffers, not just one',
24
+ '- "check the debug logs" -> get_recent_logs(tag="...")',
25
+ "",
26
+ "Key behaviors:",
27
+ "- Network/RTM buffers clear on shutdown. Clogs persist from app start.",
28
+ "- If ping fails, do NOT retry. Tell the user and fall back to code-only analysis.",
29
+ "- Fetch details eagerly. When results reveal interesting events, get their detail immediately.",
30
+ "- RTM event types use subtypes when available (e.g. message_changed, not message).",
31
+ "- Use wait_for_events with idle_timeout_ms instead of polling loops.",
32
+ "- Use ping(timeout_minutes=N) to extend sessions. Monitor remaining_minutes proactively.",
33
+ "- Screenshots default to 720px. Use full_res=true only when detail matters.",
34
+ "- Setup: /slack-radar-setup",
35
+ ].join("\n");
36
+ export const TOOL_DEFINITIONS = [
37
+ {
38
+ name: "ping",
39
+ description: "Check if Slack Radar is running. Returns uptime, buffer sizes, remaining session time, and stream status.",
40
+ inputSchema: {
41
+ type: "object",
42
+ properties: {
43
+ timeout_minutes: {
44
+ type: "number",
45
+ description: "Set or extend the auto-shutdown timeout in minutes (default: 5).",
46
+ },
47
+ },
48
+ },
49
+ },
50
+ {
51
+ name: "get_recent_network_calls",
52
+ description: "Get recent HTTP network calls captured from the Slack Android app.",
53
+ inputSchema: {
54
+ type: "object",
55
+ properties: {
56
+ limit: { type: "number", description: "Max number of calls to return (default: 50)" },
57
+ url_filter: { type: "string", description: "Filter calls by URL substring" },
58
+ status_code: { type: "number", description: "Filter by HTTP status code" },
59
+ },
60
+ },
61
+ },
62
+ {
63
+ name: "get_network_call_detail",
64
+ description: "Get full details of a specific network call by ID, including request/response headers and bodies.",
65
+ inputSchema: {
66
+ type: "object",
67
+ properties: {
68
+ id: { type: "number", description: "The call ID from get_recent_network_calls" },
69
+ },
70
+ required: ["id"],
71
+ },
72
+ },
73
+ {
74
+ name: "get_recent_rtm_events",
75
+ description: "Get recent RTM (Real Time Messaging) event summaries.",
76
+ inputSchema: {
77
+ type: "object",
78
+ properties: {
79
+ limit: { type: "number", description: "Max number of events to return (default: 50)" },
80
+ event_type: { type: "string", description: "Filter by event type/subtype" },
81
+ direction: { type: "string", description: "Filter by event direction" },
82
+ channel: { type: "string", description: "Filter by channel ID" },
83
+ },
84
+ },
85
+ },
86
+ {
87
+ name: "get_rtm_event_detail",
88
+ description: "Get full details of a specific RTM event by ID, including the complete JSON payload.",
89
+ inputSchema: {
90
+ type: "object",
91
+ properties: {
92
+ id: { type: "number", description: "The event ID from get_recent_rtm_events" },
93
+ },
94
+ required: ["id"],
95
+ },
96
+ },
97
+ {
98
+ name: "get_recent_clogs",
99
+ description: "Get recent clog (analytics) event summaries. Returns event name and timestamp. Use get_clog_detail for the full JSON payload.",
100
+ inputSchema: {
101
+ type: "object",
102
+ properties: {
103
+ limit: { type: "number", description: "Max number of clogs to return (default: 50)" },
104
+ name: { type: "string", description: "Filter by event name substring" },
105
+ },
106
+ },
107
+ },
108
+ {
109
+ name: "get_clog_detail",
110
+ description: "Get full details of a specific clog event by ID, including the complete JSON payload.",
111
+ inputSchema: {
112
+ type: "object",
113
+ properties: {
114
+ id: { type: "number", description: "The clog ID from get_recent_clogs" },
115
+ },
116
+ required: ["id"],
117
+ },
118
+ },
119
+ {
120
+ name: "wait_for_events",
121
+ description: "Wait for matching events to arrive in real-time via the SSE stream.",
122
+ inputSchema: {
123
+ type: "object",
124
+ properties: {
125
+ event_type: { type: "string", description: "RTM event type to wait for" },
126
+ channel: { type: "string", description: "Filter by channel ID" },
127
+ source: { type: "string", description: "Wait for network calls or RTM events (default: both)" },
128
+ timeout_ms: { type: "number", description: "Max time to wait in ms (default: 30000)" },
129
+ idle_timeout_ms: { type: "number", description: "Resolve after this many ms of silence (default: 3000)" },
130
+ },
131
+ },
132
+ },
133
+ {
134
+ name: "search",
135
+ description: "Search across all captured network calls and RTM events.",
136
+ inputSchema: {
137
+ type: "object",
138
+ properties: {
139
+ query: { type: "string", description: "Search term" },
140
+ limit: { type: "number", description: "Max results per category (default: 20)" },
141
+ },
142
+ required: ["query"],
143
+ },
144
+ },
145
+ {
146
+ name: "get_app_state",
147
+ description: "Get the current app state from the device.",
148
+ inputSchema: { type: "object", properties: {} },
149
+ },
150
+ {
151
+ name: "get_recent_logs",
152
+ description: "Get recent logcat entries from the connected device, filtered by tag.",
153
+ inputSchema: {
154
+ type: "object",
155
+ properties: {
156
+ tag: { type: "string", description: "Logcat tag to filter by" },
157
+ limit: { type: "number", description: "Max number of lines to return (default: 50)" },
158
+ grep: { type: "string", description: "Optional text to grep within the filtered output" },
159
+ },
160
+ required: ["tag"],
161
+ },
162
+ },
163
+ {
164
+ name: "screenshot",
165
+ description: "Capture a screenshot from the connected Android device.",
166
+ inputSchema: {
167
+ type: "object",
168
+ properties: {
169
+ full_res: { type: "boolean", description: "Capture at full resolution (default: false)" },
170
+ },
171
+ },
172
+ },
173
+ {
174
+ name: "record_screen",
175
+ description: "Record a short screen video from the connected Android device.",
176
+ inputSchema: {
177
+ type: "object",
178
+ properties: {
179
+ duration_s: { type: "number", description: "Recording duration in seconds (default: 5, max: 30)" },
180
+ },
181
+ },
182
+ },
183
+ {
184
+ name: "clear_buffers",
185
+ description: "Clear all captured network calls and RTM events from both device and local buffers.",
186
+ inputSchema: { type: "object", properties: {} },
187
+ },
188
+ {
189
+ name: "_radar_session_enable",
190
+ description: "Enable Slack Radar for this session. Sets up ADB forwarding, activates the device-side server, and connects the SSE stream. Called by /radar-enable skill.",
191
+ inputSchema: {
192
+ type: "object",
193
+ properties: {
194
+ timeout_minutes: { type: "number", description: "Device-side auto-shutdown timeout in minutes (default: 5)" },
195
+ },
196
+ },
197
+ },
198
+ {
199
+ name: "_radar_session_disable",
200
+ description: "Disable Slack Radar for this session. Tears down the SSE stream and clears local buffers. Called by /radar-disable skill.",
201
+ inputSchema: { type: "object", properties: {} },
202
+ },
203
+ ];
204
+ /**
205
+ * Check whether a tool call should be blocked by the session gate.
206
+ * Returns an error response object if blocked, or null if allowed.
207
+ */
208
+ export function sessionGate(toolName, sessionEnabled) {
209
+ // Session enable/disable are always allowed
210
+ if (toolName === "_radar_session_enable" || toolName === "_radar_session_disable") {
211
+ return null;
212
+ }
213
+ // ping is allowed even when disabled
214
+ if (toolName === "ping") {
215
+ return null;
216
+ }
217
+ if (!sessionEnabled) {
218
+ return {
219
+ content: [
220
+ {
221
+ type: "text",
222
+ text: JSON.stringify({
223
+ error: "Radar is not active. Use /radar-enable to connect, or ask about the device to auto-enable.",
224
+ }),
225
+ },
226
+ ],
227
+ isError: true,
228
+ };
229
+ }
230
+ return null;
231
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Platform-agnostic interface for device communication.
3
+ *
4
+ * Each platform (Android, iOS) implements this interface to handle
5
+ * port forwarding, server activation, screenshots, logs, and other
6
+ * device-specific operations. The MCP server and tool handlers
7
+ * interact only through this interface.
8
+ */
9
+ export interface DeviceTransport {
10
+ /** Human-readable platform name (e.g. "android", "ios"). */
11
+ readonly platform: string;
12
+ /**
13
+ * Set up port forwarding so the device's radar HTTP server is
14
+ * reachable at localhost:RADAR_PORT. Returns true on success.
15
+ */
16
+ ensureForward(): boolean;
17
+ /**
18
+ * Activate the on-device radar server. May be a no-op if the
19
+ * platform activates automatically.
20
+ *
21
+ * @param timeoutMinutes Auto-shutdown timeout in minutes
22
+ */
23
+ activate(timeoutMinutes: number): void;
24
+ /** Reset all connection state (forwarding, activation, caches). */
25
+ resetState(): void;
26
+ /** Whether forwarding has been established. */
27
+ isForwarded(): boolean;
28
+ /** Current auto-shutdown timeout in minutes. */
29
+ getTimeoutMinutes(): number;
30
+ /**
31
+ * Capture a screenshot from the connected device.
32
+ * Returns the local file path to the saved image.
33
+ *
34
+ * @param fullRes If true, capture at full resolution
35
+ */
36
+ screenshot(fullRes: boolean): string;
37
+ /**
38
+ * Record a short screen video from the connected device.
39
+ * Returns the local file path to the saved video.
40
+ *
41
+ * @param durationS Recording duration in seconds
42
+ */
43
+ recordScreen(durationS: number): string;
44
+ /**
45
+ * Get the current app state from the device (visible screens,
46
+ * fragments, arguments, etc.). Returns a human-readable string.
47
+ */
48
+ getAppState(): string;
49
+ /**
50
+ * Get recent log entries filtered by tag.
51
+ *
52
+ * @param tag Log tag to filter by
53
+ * @param limit Max number of lines
54
+ * @param grep Optional text to further filter
55
+ * @returns Array of matching log lines
56
+ */
57
+ getRecentLogs(tag: string, limit: number, grep?: string): string[];
58
+ }
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@slack/radar-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for on-device debugging of the Slack Android app via ADB",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "slack-radar-mcp": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "scripts": {
14
+ "clean": "rm -rf dist",
15
+ "prebuild": "npm run clean",
16
+ "build": "tsc && chmod +x dist/index.js",
17
+ "start": "node dist/index.js",
18
+ "watch": "tsc --watch",
19
+ "test": "npm run build && node --test tests/*.test.mjs",
20
+ "test:smoke": "npm run build && bash tests/smoke.sh",
21
+ "test:all": "npm run test && npm run test:smoke",
22
+ "prepare": "npm run build"
23
+ },
24
+ "keywords": [
25
+ "mcp",
26
+ "slack",
27
+ "android",
28
+ "debugging",
29
+ "adb"
30
+ ],
31
+ "engines": {
32
+ "node": ">=18"
33
+ },
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://slack-github.com/slack/slack-radar-mcp"
37
+ },
38
+ "bugs": {
39
+ "url": "https://slack-github.com/slack/slack-radar-mcp/issues"
40
+ },
41
+ "homepage": "https://slack-github.com/slack/slack-radar-mcp#readme",
42
+ "license": "MIT",
43
+ "dependencies": {
44
+ "@modelcontextprotocol/sdk": "^1.12.1"
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "^22.10.5",
48
+ "typescript": "^5.7.2"
49
+ }
50
+ }