@slack/radar-mcp
v1.21.0
Published
MCP server and web dashboard for debugging Slack on Android and iOS Simulator
Readme
slack-radar-mcp
Platform-agnostic MCP server and web dashboard for on-device debugging of mobile apps.
Gives Claude Code direct access to network calls, RTM events, analytics (clogs), screenshots, logs, and more from a running debug build. Ships with Android device support (ADB) and initial iOS Simulator support; the DeviceTransport interface keeps platform-specific host behavior isolated.
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.
Installation
The dashboard and the agent tools are set up separately. install-app gives you the dock app (see Install it to your dock below); the tools need registering in every client you use.
For Claude Code, the recommended path is the marketplace plugin:
/plugin install slack-radar@slack-android-marketplace
/slack-radar-setupOr register it in Claude Code manually:
slack claude mcp add -s user slack-radar-mcp -- npx -y @slack/radar-mcp@latestEvery other MCP client registers servers its own way, in its own config format, but the entry is the same shape everywhere: a command of npx, with the arguments -y and @slack/radar-mcp@latest. Check your client's own documentation for which file that entry belongs in and what syntax it expects.
Restart the client for the tools to appear, whichever one you registered with.
Web Dashboard
The package also ships a browser-based dashboard that shows the same live device data without Claude Code in the loop.
npx @slack/radar-mcp webOr with a global install:
npm install -g @slack/radar-mcp
slack-radarOpens http://localhost:8100 automatically. Set SLACK_RADAR_WEB_PORT to use a different port. (slack-radar-web still works as a direct alias.) Tabs:
- Live — network, RTM, analytics clogs, traces, Slack-app logcat and Switchboard voice records in one timeline (level ≥ applies to the logcat lines). The rate and sparkline count every source together, so a voice session raises them the way RTM traffic does
- Network — calls per second vs response time, every call listed, slow ones flagged
- RTM — real-time websocket events
- Clogs — analytics events, click one to see its payload
- Traces — performance trace spans with durations, click one for its timing and tags
- Logs — device logcat, filtered to the Slack app. app switches app or shows all apps; level ≥ sets the lowest level to show (VERBOSE shows everything; the choice follows you between Logs and Live and waits while you visit another tab)
- Switchboard — the Slackbot voice transport's own records: session begin and end, connection, mic and speaking state changes, every websocket frame (← in, → out) with type and size, phase timings, and player diagnostics (re-arms, jitter trims, read stalls, pre-ready drops). Click a row for the rest of its fields; no level, so hide noise with the filter box or exclude, and the kinds chips on the toolbar turn one kind of record off at a time. Records arrive once a Slackbot voice session starts. A debug build that declares its sources and lacks the plugin hides the tab; one too old to declare them shows it empty. Above the feed, a session panel draws the newest session: connection, mic and speaking pills, the handshake waterfall, model/you tracks with markers for re-arms, tts.stop and focus loss, and four stat cards, at a 60 s, 5 min or whole session zoom; the audio levels are not drawn.
- Database — read-only browser for the on-device SQLite stores (pick a database, then a table, or run a read-only query). See the data-handling note below.
- Preferences — read-only browser for the on-device SharedPreferences files (pick a file, browse its keys, or search across every file at once).
- Feature Flags — live view of every feature flag and experiment the Slack app knows about, with its current effective value (ON/OFF for toggles, group name for experiments), its scope, and whether it is locally overridden; updates as they change on the device. Filter by name.
- Custom — a tab Claude builds on request. Ask a Claude Code session in your terminal for a view the pre-built tabs do not cover (e.g. "graph calls per second and flag the slow ones"); it authors the view via the
open_radar_dashboardtool and it appears here. A Custom view can carryexcludeterms to hide noisy rows; add more with the exclude… control on the feed toolbar or the Filters panel (comma-separated there), or have the agent set them in the spec (one phrase per term, no commas or line breaks). A log view narrowed to one tag is the same kind of ask: "show me only the<Tag>logs, DEBUG and up, without the noisy lines" becomes a spec withsource: log,match: {package, tag},minLevelandexclude, opened as a Custom tab. Persists until you refresh the page; exclude terms are remembered in the browser only on the pre-built tabs.
Each tab has a filter / sort / pause / clear feed and a payload inspector, most a live rate, Network and Traces a graph. Clear empties the dashboard's own view only, so the device keeps its buffers and a reconnect or a source switch backfills recent records again (ask a Claude Code session for clear_buffers to wipe the device too). The filter box takes space-separated terms that must all match, -term to exclude, /pattern/ for regex, and "quoted phrases" for spaces or a literal - or / (curly quotes count too, and pasting text with spaces quotes it for you, unless you are mid-word or the text starts in filter syntax). -term is page-wide and gone on reload; to keep an exclusion, use the exclude… control on the feed toolbar (one term, then Enter), the exclude row in the Filters panel (comma separated) or an agent's exclude on the spec: the terms show above the feed as removable chips, are saved with the tab, and on a pre-built tab are remembered in the browser when you come back; a spec an agent pushes lands as written and does not touch that memory. An exclude term written field:value (say kind:frame, the Switchboard record's own kind field; no space after the colon) hides rows whose field equals the value, case-insensitive, and a field no row carries hides nothing; plain terms match by substring, and the Switchboard tab's kind chips toggle those kind: terms for you. The ? at the right edge of the box opens a short legend under it. A phrase that itself contains a quote character goes in /regex/ form instead, where quotes are plain text. Committed terms turn into removable pills; a leading - is now the exclude operator (the old box matched it as plain text), and quoting restores the literal. Regex flags are optional and short (i/m/s/g, 1-2 letters only, so an ambiguous tail like /api/im reads as flags, noted in the tooltip); typos like /error/x become part of the pattern and match literally, and quoting any /…/ term matches it literally. Invalid regexes and incomplete typing (e.g. /foo() are ignored (the term turns amber) instead of blanking the feed. On the Live tab a row type name (log, clog, trace, rtm, network, switchboard) also matches rows of that type, and the -log form excludes by row type only, so it hides logcat lines without touching a logout clog (quote it, -"log", to exclude by text instead). Switching tabs replays what you have already seen this session (the browser caches it, bounded by count and age) so the view does not reset to empty. ADB forwarding and device activation are handled for you; a reconnect control re-arms after a device sleep. The device-side session shuts down after its idle timeout, and an open dashboard tab now recovers that on its own: the header says there is no radar session on the device and re-enables it, once right away (or on the minute, if the session just came back and died again) and then at most once a minute, giving up after three failed attempts (a session that dies again within a minute of coming back counts as a failed attempt), or at once if the attached device changed, so the next press is yours. Only a tab you are looking at does this (a background tab never arms a device you let idle out), never a dashboard an agent opened, and ✋ stop auto re-enable in the header turns it off for dashboards at this address if you would rather do it by hand (SLACK_RADAR_NO_ARM=1 on the process turns it off for that process, along with the arm at launch). The dashboard follows your system light/dark setting by default; the theme toggle in the header overrides it and is remembered. A form-encoded request body is shown decoded, field by field, JSON fields expanded; Copy cURL still carries the raw bytes. A field whose JSON nests too deep stays as text with a note saying so, and a request body the device cap cut shows a truncation banner.
Install it to your dock (macOS)
To open Radar from the dock instead of a terminal:
npx @slack/radar-mcp install-appThat writes ~/Applications/Slack Radar.app. Open it once, then right-click its dock icon and choose Options > Keep in Dock.
Clicking it starts the dashboard if it is not already running (the server auto-closes 5 minutes after the last tab closes, so this is the usual case), waits for the port, then shows it. If an older dashboard is still holding the port and nobody is using it, the app asks that one to stop first, so an upgrade does not leave you looking at the version you replaced. Quitting the app stops the dashboard it started, unless a Claude Code session or another window is still using it; a dashboard that was already running when the app launched is never stopped. If Xcode's command line tools are installed (xcode-select --install), install-app compiles a small native window for this — its own dock icon, its own process, no browser tab, no tab bar. Without a Swift toolchain it falls back to opening your default browser instead.
If the window cannot start the dashboard after a node upgrade, the app is holding a node path that the upgrade moved. Run this, then quit and reopen the app:
npx -y @slack/radar-mcp@latest install-appScreen overlay
A ▣ Screen button opens a side panel that mirrors the live device screen, with a full-resolution screenshot and a screen recording. It is off by default and opens to a consent notice first, because it shows whatever is on the phone (open DMs, message content, notifications).
- Screenshot needs no extra tools: Android uses
screencap, and iOS Simulator usesxcrun simctl io. - Live mirror uses the Android
screenrecordstream (needsffmpeg). For iOS Simulator, Radar polls native screenshots at 1fps — no ffmpeg dependency. - Recording uses
ffmpegon Android andsimctl recordVideoon iOS Simulator. If ffmpeg is missing on Android, the panel shows the install command. ffmpeg is not bundled and is never installed for you.
Prerequisites
- Node.js 18+
- ADB for Android, or Xcode (full installation, not just command-line tools) for iOS Simulator
- A debug build of Slack installed on the device (must include the
slack-radarmodules)
Environment Variables
All optional. Set them on the process that runs the MCP server or the dashboard. One further variable, SLACK_RADAR_INITIAL_SPEC, is set automatically when a tool spawns the dashboard and is not meant to be set by hand.
| Variable | Effect |
|---|---|
| SLACK_RADAR_WEB_PORT | Port the dashboard listens on. Default 8100. |
| SLACK_RADAR_DEVICE_PORT | Host port forwarded to the device-side server. Default 8099. |
| SLACK_RADAR_NO_AUTO_CLOSE=1 | Keep the dashboard running forever instead of closing it once every tab is gone. |
| SLACK_RADAR_ALLOWED_HOSTS | Extra Host values the dashboard will answer, comma-separated (radar.local,myhost:8100). Only needed when you reach it by a name other than localhost. See Security. |
| SLACK_RADAR_NO_ARM=1 | Never arm the device without a press: skips the arm at launch and turns off a tab's automatic re-enable. A Reconnect press still arms. |
| SLACK_RADAR_NO_OPEN=1 | Do not open a browser window on launch. Set automatically when an MCP tool spawns the dashboard. |
| SLACK_RADAR_NO_FFMPEG=1 | Pretend ffmpeg is absent, so screen recording degrades to screenshots. |
| SLACK_RADAR_PLATFORM | Transport to use: android (the default) or ios. Radar does not probe or infer a different platform. |
| SLACK_RADAR_IOS_SIMULATOR_UDID | Pin a specific iOS Simulator by UDID instead of auto-selecting the sole booted one. |
Session Management
Radar starts disabled each session. All tools except ping return an error until enabled.
| Command | Effect |
|---|---|
| /radar-enable | Activates ADB forwarding, device broadcast, SSE stream. Tools become available. |
| /radar-disable | Tears down stream, clears local buffers. Tools return "not active" error. |
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.
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.
Tools
| Tool | Description |
|---|---|
| ping | Health check. Returns uptime, buffer sizes, remaining session time, stream status, the Radar MCP version (mcp_version), and which app/profile is bound (android_user_id, profile personal/secondary, package). Use timeout_minutes to extend sessions. |
| get_recent_network_calls | HTTP call summaries: method, URL, status code, duration. Filterable by URL substring and status code. |
| get_network_call_detail | Full request/response headers and bodies for a specific call (gzip/brotli decompressed). A form-encoded request body (by the record's request_content_type, or by shape on a build without it) also comes back decoded in request_body_decoded, JSON-valued fields parsed. A body, or its decoded copy, over 64 KB skips the decoded copy and request_body_decoded_omitted says so; a field left unparsed is named in request_body_decoded_unparsed. request_body stays the raw bytes; request_body_truncated: true means the device cap cut it (a build that reports the cut itself is believed, otherwise it is read from the body's shape). |
| query_session | The whole session from the host event spool (network, RTM, clogs, traces, switchboard), newest first, byte-capped, with a cursor to page older. An id read returns the full record, pulling the body from the device once and caching it; a network record gets the same decoded-form fields as get_network_call_detail; a body that cannot be pulled falls back to the summary with a note. |
| get_recent_rtm_events | RTM event summaries: type, direction, channel, timestamp. Filterable by type, direction, channel. Uses subtypes (e.g. message_changed, not message). |
| get_rtm_event_detail | Full RTM event JSON payload for a specific event. |
| get_recent_clogs | Analytics event summaries: name and timestamp. Filterable by event name substring. Persists from app start. |
| get_clog_detail | Full clog JSON payload for a specific event. |
| get_recent_traces | Performance trace/span summaries: trace name, span name, duration. Filterable by name substring. |
| get_trace_detail | Full trace/span detail for a specific trace, including tags. |
| get_switchboard_sessions | Slackbot voice (switchboard) sessions the device still holds, newest first: session_id, team_id, began_at, ended_at, record and frame counts, the newest state and audio levels. Ids restart with the app process. |
| get_switchboard_session | One voice session's summary plus its records, oldest first: state transitions, control frames (frame) in and out, player stutter events, phase startup timings, session begin and end. Narrow with kind; limit is how many of the newest records come back (default 50, max 2000), filtered by kind first, so a kind page is the newest limit records of that kind. |
| wait_for_events | Block until matching events arrive via SSE stream. Idle-timeout based, not polling. |
| search | Full-text search across network, RTM, clog, and trace buffers. |
| get_app_state | Current Activity/Fragment state via dumpsys. Shows visible screen, parameters, tab state. |
| get_recent_logs | Recent device logs, optionally filtered by tag (omit tag for all lines). Reads the app's own debug log buffer first, falling back to logcat; the result's source field (app_buffer / logcat_capture / logcat_ring) names which one served it. For Timber debug logs (SlackRadar, OkHttp, etc.). |
| list_profiles | List the Android users and profiles on the device (personal, work, and so on). |
| activate_for_user | Activate radar for a specific Android user or profile. |
| capture_snapshot | Bundle the current network, RTM, clog, and trace buffers, plus an optional screenshot, into one saved snapshot for a bug report or repro. Switchboard voice records are not included. |
| screenshot | Capture device screen as PNG. 720px default, full_res=true for full resolution. |
| record_screen | Record short screen video (MP4). Default 5s, max 30s. |
| list_databases | List the app's on-device SQLite databases. Read-only. |
| list_tables | List the tables in a given on-device database, with row counts. Read-only. |
| query_database | Run a read-only query (SELECT, WITH, EXPLAIN, VALUES, PRAGMA) against a copy of an on-device database pulled to the host. |
| list_prefs | List the app's on-device SharedPreferences files. Read-only. |
| get_prefs | Read a preferences file's keys and values, optionally filtered to specific keys. |
| search_prefs | Search preference keys and values across every file by a substring. |
| list_feature_flags | List every feature flag with its live effective value and override status. Optionally filtered to just the overridden ones. Boolean toggles carry enabled; multi-variant experiments carry type: "experiment" and a group_name string (the effective variant, e.g. "treatment_a") instead. |
| search_feature_flags | Case-insensitive substring search on feature flag names, against the same live list as list_feature_flags. Rows carry the same type/group_name fields for experiments. |
| get_feature_flag | Look up one feature flag by its exact key. Returns one row per scope (authenticated/unauthenticated) and per signed-in org, since each workspace resolves its own value; pass org_id to narrow to one workspace. An experiment row carries type: "experiment" and its group_name variant rather than a boolean enabled. |
| clear_buffers | Clear all captured buffers (network, RTM, clog, trace) on the device and locally. |
| open_radar_dashboard | Author or refine a custom live dashboard tab from a spec (source, filters, widgets, alerts). Suggested when a request is not covered by the pre-built tabs; not auto-invoked. Returns the dashboard URL; debug builds only, local only. |
| _radar_session_enable | Enable radar for the session. Called by /radar-enable skill. |
| _radar_session_disable | Disable radar for the session. Called by /radar-disable skill. |
Architecture
Mobile debug build (device)
+-- Local socket (platform-specific)
+-- HTTP endpoints + SSE streaming
|
| port forwarding (adb forward / iproxy / etc.)
|
@slack/radar-mcp (Node.js, runs on developer machine)
+-- src/shared/ DeviceTransport, AndroidTransport, SSE stream, constants
+-- src/mcp/ MCP stdio server (Claude Code entry point)
+-- src/web/ HTTP proxy server + browser dashboard
+-- public/ Static HTML/CSS/JS served to the browserAll device-specific logic lives behind the DeviceTransport interface (src/shared/transport.ts). Buffers, SSE streaming, session management, and the HTTP wire protocol are platform-agnostic: the wire format is plain GET /api/<resource> with JSON, and adb appears only in the transport.
Consuming the wire from your own tooling: the dashboard's HTTP+SSE surface is documented as a read contract in docs/wire-contract.md (Oracle Surface v1) — named routes, SSE frame grammar, backfill/dedupe recipe, the auto-close trap, and an executable preflight (scripts/preflight.mjs, which lives in this repo rather than the published package, and is meant to be copied into your own harness).
Adding a new platform: implement DeviceTransport in src/shared/<platform>.ts and add a case to createTransport() in src/mcp/index.ts. The device-shaped operations (screenshot, screen recording, app state, device logs) are optional members, so implement what the platform supports and leave the rest absent; callers report each absent capability as "not supported on platform X". Two things stay Android-only for now and are guarded rather than abstracted: the dashboard (its flow assumes a device to arm) and adb port forwarding.
Security
- No TCP port on device. Uses platform-specific local sockets, only reachable via port forwarding. Requires physical USB or explicit debug pairing.
- Debug builds only. Release builds bind a no-op implementation. Server code is eliminated at compile time.
- Lazy activation. Dormant until explicitly triggered.
- Auto-shutdown. Device-side server shuts down after idle timeout (default 5 minutes). Zero cost when unused.
- Dashboard auto-close. Once a dashboard has been opened in a browser, the process shuts itself down roughly 6 minutes after the last tab closes, so a dashboard you walked away from does not linger. Opening a dashboard again (
open_radar_dashboard, which writes a spec) pushes that deadline out by another grace window, but nothing else does: the other MCP tools read the device directly and never touch the dashboard, so a long agent session with no tab open will still close on schedule. Reopen it, or use the opt-out below, if you want it to stay. Two more exceptions: a dashboard that no tab has ever opened (one an agent spawned and nobody visited) stays up until you visit it once or stop it, because there is no "last tab closed" moment to measure from; and when you first upgrade to a version that has auto-close, reload any tab that was already open — page code from before this feature does not report itself as present, so it will not keep the process alive even while you are looking at it. Reloading once is enough; later upgrades do not need it. Click ⏻ Stop dashboard in the header to stop it immediately instead (click twice to confirm — it kills the process with no undo). SetSLACK_RADAR_NO_AUTO_CLOSE=1to turn auto-close off entirely. - Keep-alive is not forgeable from a web page. Only the dashboard's own client and the MCP can postpone auto-close. A write to
/setspecor/enablehas to carryX-Radar-Dashboard: 1, or a content-type whose type is exactlyapplication/json; the/healthpresence ping requires the header specifically, since a content-type says nothing about a request with no body. A cross-origin request cannot set either one without a CORS preflight it will fail. A request without either still works and still writes, it just does not extend the process lifetime, socurl -X POST .../setspec -H 'Content-Type: text/plain'sets the spec but will not stop the dashboard closing on schedule; use-H 'Content-Type: application/json'if you want it to count. This is a CSRF guard, not authentication: any local process can send either header, and the port is loopback-only by design. - Loopback
Hostonly. The dashboard answers a request only when itsHostheader names this machine (localhost,127.0.0.1,[::1],0.0.0.0, with or without a port). Anything else gets a 403 whose body names the host it refused and how to allow it. Binding to 127.0.0.1 alone is not enough: a page that resolves its own domain to 127.0.0.1 connects locally, counts as same-origin in the browser, and could otherwise set the header above and stop your dashboard. If you reach the dashboard by some other name — a/etc/hostsalias, a container or Codespaces forwarded URL, your machine's LAN hostname — list it inSLACK_RADAR_ALLOWED_HOSTS(comma-separated, host orhost:port). A plain SSH or VS Code port forward needs nothing, because theHoststayslocalhost.
Data Capture
What gets captured depends on the in-app server implementation. The MCP server is agnostic to the data source. The Android implementation captures:
- Network calls: OkHttp interceptor. Ring buffer: 400 calls.
- RTM events: WebSocket listener. Ring buffer: 2000 events.
- Clogs: Analytics interceptor. Ring buffer: 1000 events. Persists across radar activation cycles.
- Traces: Performance span observer. Ring buffer: 2000 spans. Persists across radar activation cycles.
- SQLite database (Database tab, web dashboard only): when you open a database, a read-only plaintext copy of that on-device store (message text, request/response bodies, user/channel/team IDs) is pulled to a temp directory on your own machine (
os.tmpdir()/slack-radar-db-<port>, files mode 0600). Auto-deleted when the dashboard process stops. - SharedPreferences (
list_prefs/get_prefs/search_prefs, and the Preferences tab): read directly off the device overadb, no local copy written. Files can contain feature flag overrides, cached team/org/channel IDs, and other app state. - Feature flags (
list_feature_flags/search_feature_flags/get_feature_flag): read directly off the device overadb, no local copy written. Shows every flag's live effective value and whether it is locally overridden.
The network, RTM, clog, and trace captures are in-memory ring buffers. The Database tab is the exception: it writes a plaintext copy of the selected on-device database to a temp file on your host for the duration of the session. Review before sharing anything from it.
What gets captured, in detail
This tool captures unredacted network traffic from your debug Slack build, including:
- URL query parameters (tokens can appear here)
Authorizationheaders and cookiesSet-Cookieresponse headers (session tokens)- Request and response bodies, up to 256 KB per body, for JSON, form, text, and XML content types
- RTM WebSocket payloads (every sent and received message)
- Analytics (clog) events serialized as JSON
- Screenshots and short screen recordings on demand
- Device logs (the app's own debug buffer, with a logcat fallback), optionally filtered by tag
- SharedPreferences files, including feature flag overrides and cached team/org/channel IDs
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.
Use against workspaces you are signed into as yourself
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.
Local Development
npm install
npm run build # Compile TypeScript + copy static assets to dist/
npm start # Run the MCP server (stdio transport)
npm run start:web # Run the web dashboard
npm run watch # TypeScript compiler in watch mode
npm test # Unit tests
npm run test:smoke # Smoke tests
npm run test:all # BothRadar uses the Android transport by default. Choose the iOS transport explicitly when working with a Simulator:
npm run start:web
SLACK_RADAR_PLATFORM=ios npm run start:web
# Equivalent: npm run start:web -- --platform=iosWithin the selected platform, Radar chooses the sole running device or shows the device picker
when several are available. It never probes the other platform. iOS support is currently limited
to screenshot, screen mirroring, and recording via simctl; the HTTP-based tools (network,
RTM, clogs) require a device-side server that is not yet available on iOS.
Troubleshooting
"ADB port forwarding failed"
Ensure a device is connected (adb devices should show exactly one device). If using wireless debug, confirm the pairing is active.
"Radar not responding"
The device-side server may have auto-shut down. Use /radar-enable to restart it.
"Session disabled"
Two consecutive connection failures disable the session. Use /radar-enable to reconnect.
Devices with more than one user (e.g. a work profile)
The MCP server auto-detects the Android user ID via ps and targets the broadcast accordingly. No manual --user flag needed. In the dashboard's Logs tab, the package filter narrows logcat to the Slack app; clear the chip or pick another package to widen it.
Tools not appearing after setup Restart your Claude Code session. MCP servers are registered at session start.
Port already in use (web dashboard)
If a Radar dashboard is already running on the port, launching again just opens it. If the port is held by another program, run Radar on a different port with SLACK_RADAR_WEB_PORT=8101 npx @slack/radar-mcp web.
The dashboard shows the version you just replaced That badge reports the version of the process serving the port, not the app or package you installed. Installing does not touch a server that is already running, so an older dashboard can keep serving after an upgrade.
The app also leaves an older dashboard alone on purpose when somebody else is using it, either a Claude Code session or another window with it open. A refresh (⌘R in the app window, or Retry on its failure page) tries to take it from a window, and never from a session; taking it costs that window its page, and everyone the server's pulled databases, device pin and logcat ring. ~/Library/Logs/slack-radar-dashboard.log names which one it found, and what a refresh will do about it.
Ask the two surfaces separately. The installed dock app:
defaults read ~/Applications/Slack\ Radar.app/Contents/Info.plist CFBundleShortVersionStringAnd whatever is actually serving, with the dashboard open. A GET with the headers printed, not curl -sI: a HEAD only answers on the release that carries this change and later, and the dashboard you are diagnosing is an older one by definition.
curl -s -D - -o /dev/null http://127.0.0.1:8100/spec | grep -i x-radar-versionUse your own port if you set SLACK_RADAR_WEB_PORT. Against the wrong port both commands print nothing, which looks the same as a dashboard that is not there.
Two different versions is the answer. Stop the old dashboard, then quit the app and open it again:
curl -s -X POST -H 'X-Radar-Dashboard: 1' http://127.0.0.1:8100/shutdownIf it comes back older, the dashboard is being started by your Claude Code session, so restart that to reload its MCP server. Where npm enforces a minimum release age, @latest will not resolve to a release for its first week; pin the version in your MCP registration to get it sooner.
