@slack/radar-mcp 1.3.0 → 1.5.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.
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Dashboard spec model. A "spec" is the declarative description of one dashboard
3
+ * tab: which device stream to read, how to filter it, and which widgets to show.
4
+ * The web client polls `/spec`, the server persists the active spec via `/setspec`,
5
+ * and the same validate/coerce pass runs on every push so a malformed spec surfaces
6
+ * an error instead of rendering silent zeros.
7
+ *
8
+ * Most sources are live STREAMS (network/rtm/clog/log/all). `db` is the odd one out:
9
+ * a read-only, point-in-time SQLite BROWSER over the on-device databases, with no viz /
10
+ * series / match (it has its own list -> tables -> query-grid view). The coerce and
11
+ * validate passes short-circuit for `db` accordingly.
12
+ */
13
+ /**
14
+ * Sources the dashboard can read. The first five are live streams; "all" is their unified
15
+ * timeline. "db" is a read-only on-device SQLite browser (a point-in-time snapshot, not a
16
+ * stream) and is handled as a distinct view.
17
+ */
18
+ export declare const SOURCES: readonly ["rtm", "network", "clog", "log", "all", "db"];
19
+ export type Source = (typeof SOURCES)[number];
20
+ /** Widgets a spec may compose. */
21
+ export declare const VIZ: readonly ["counter", "rate", "sparkline", "feed", "inspector", "graph"];
22
+ export type Viz = (typeof VIZ)[number];
23
+ /** Metrics a graph series can plot. */
24
+ export declare const SERIES_METRICS: readonly ["ratePerSec", "count", "avg", "errorRate"];
25
+ export type SeriesMetric = (typeof SERIES_METRICS)[number];
26
+ declare const HIGHLIGHT_OPS: readonly [">", ">=", "<", "<=", "=="];
27
+ export type HighlightOp = (typeof HIGHLIGHT_OPS)[number];
28
+ export interface GraphSeries {
29
+ label?: string;
30
+ metric: SeriesMetric;
31
+ field?: string;
32
+ unit?: string;
33
+ }
34
+ export interface Highlight {
35
+ field: string;
36
+ op?: HighlightOp;
37
+ value: number;
38
+ label?: string;
39
+ }
40
+ export interface Extract {
41
+ kind?: Source;
42
+ parse?: "response_body" | "request_body" | "data";
43
+ field: string;
44
+ label?: string;
45
+ }
46
+ export interface Alert {
47
+ sound?: boolean;
48
+ flash?: boolean;
49
+ say?: string;
50
+ at?: number;
51
+ }
52
+ /**
53
+ * A dashboard spec. `blank: true` is the valid empty state (nothing rendered yet).
54
+ * `match` is a case-insensitive substring filter keyed on summary fields of the
55
+ * chosen source.
56
+ */
57
+ export interface Spec {
58
+ title?: string;
59
+ summary?: string;
60
+ blank?: boolean;
61
+ source?: Source;
62
+ match?: Record<string, string>;
63
+ bodyContains?: string;
64
+ viz?: Viz[];
65
+ rateWindowSec?: number;
66
+ series?: GraphSeries[];
67
+ extract?: Extract;
68
+ highlight?: Highlight;
69
+ alert?: Alert;
70
+ db?: string;
71
+ table?: string;
72
+ sql?: string;
73
+ [key: string]: unknown;
74
+ }
75
+ /** The empty dashboard. Rendered until a spec is pushed. */
76
+ export declare const BLANK_SPEC: Spec;
77
+ /**
78
+ * Repair a spec into a renderable shape where the intent is recoverable, rather
79
+ * than rejecting it outright. The interpreter (and hand edits) sometimes emit
80
+ * `match` as a bare string instead of a `{field: value}` object, or a `graph`
81
+ * viz without a `series` array; coerce those into the natural form so the
82
+ * dashboard degrades gracefully. Returns the same object (mutated) for parity
83
+ * with the PoC; callers pass a throwaway parse result.
84
+ */
85
+ export declare function coerceSpec(input: unknown): Spec;
86
+ /**
87
+ * Validate a (coerced) spec. Returns null when valid, or a human-readable reason
88
+ * string when not. A bad spec must surface as an error, never as silent zeros.
89
+ */
90
+ export declare function validateSpec(s: unknown): string | null;
91
+ export {};
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Dashboard spec model. A "spec" is the declarative description of one dashboard
3
+ * tab: which device stream to read, how to filter it, and which widgets to show.
4
+ * The web client polls `/spec`, the server persists the active spec via `/setspec`,
5
+ * and the same validate/coerce pass runs on every push so a malformed spec surfaces
6
+ * an error instead of rendering silent zeros.
7
+ *
8
+ * Most sources are live STREAMS (network/rtm/clog/log/all). `db` is the odd one out:
9
+ * a read-only, point-in-time SQLite BROWSER over the on-device databases, with no viz /
10
+ * series / match (it has its own list -> tables -> query-grid view). The coerce and
11
+ * validate passes short-circuit for `db` accordingly.
12
+ */
13
+ /**
14
+ * Sources the dashboard can read. The first five are live streams; "all" is their unified
15
+ * timeline. "db" is a read-only on-device SQLite browser (a point-in-time snapshot, not a
16
+ * stream) and is handled as a distinct view.
17
+ */
18
+ export const SOURCES = ["rtm", "network", "clog", "log", "all", "db"];
19
+ /** Widgets a spec may compose. */
20
+ export const VIZ = [
21
+ "counter",
22
+ "rate",
23
+ "sparkline",
24
+ "feed",
25
+ "inspector",
26
+ "graph",
27
+ ];
28
+ /** Metrics a graph series can plot. */
29
+ export const SERIES_METRICS = [
30
+ "ratePerSec",
31
+ "count",
32
+ "avg",
33
+ "errorRate",
34
+ ];
35
+ const HIGHLIGHT_OPS = [">", ">=", "<", "<=", "=="];
36
+ /** The empty dashboard. Rendered until a spec is pushed. */
37
+ export const BLANK_SPEC = { title: "", blank: true, viz: [] };
38
+ function isRecord(v) {
39
+ return typeof v === "object" && v !== null && !Array.isArray(v);
40
+ }
41
+ /**
42
+ * Repair a spec into a renderable shape where the intent is recoverable, rather
43
+ * than rejecting it outright. The interpreter (and hand edits) sometimes emit
44
+ * `match` as a bare string instead of a `{field: value}` object, or a `graph`
45
+ * viz without a `series` array; coerce those into the natural form so the
46
+ * dashboard degrades gracefully. Returns the same object (mutated) for parity
47
+ * with the PoC; callers pass a throwaway parse result.
48
+ */
49
+ export function coerceSpec(input) {
50
+ if (!isRecord(input))
51
+ return BLANK_SPEC;
52
+ const s = input;
53
+ // The DB browser has no match/viz/series to repair; leave its db/table/sql untouched.
54
+ if (s.source === "db")
55
+ return s;
56
+ if (typeof s.match === "string") {
57
+ // "all" mixes types whose summary fields differ, so a single field guess
58
+ // would wrongly exclude other types; treat a bare-string match on "all"
59
+ // as no filter.
60
+ if (s.source === "all") {
61
+ delete s.match;
62
+ }
63
+ else {
64
+ const field = s.source === "rtm"
65
+ ? "event_type"
66
+ : s.source === "clog"
67
+ ? "event_name"
68
+ : "url";
69
+ s.match = { [field]: s.match };
70
+ }
71
+ }
72
+ // graph series repair: fill if missing/empty, trim to the validator's 1-2 cap,
73
+ // and drop entries with an unusable metric so the dashboard never hard-rejects.
74
+ if (Array.isArray(s.viz) && s.viz.includes("graph")) {
75
+ if (Array.isArray(s.series)) {
76
+ s.series = s.series.filter((x) => isRecord(x) &&
77
+ SERIES_METRICS.includes(x.metric) &&
78
+ !(x.metric === "avg" && !x.field));
79
+ if (s.series.length > 2)
80
+ s.series = s.series.slice(0, 2);
81
+ }
82
+ if (!Array.isArray(s.series) || s.series.length === 0) {
83
+ s.series = [{ label: "per second", metric: "ratePerSec", unit: "/s" }];
84
+ }
85
+ }
86
+ return s;
87
+ }
88
+ /**
89
+ * Validate a (coerced) spec. Returns null when valid, or a human-readable reason
90
+ * string when not. A bad spec must surface as an error, never as silent zeros.
91
+ */
92
+ export function validateSpec(s) {
93
+ if (!isRecord(s))
94
+ return "not an object";
95
+ const spec = s;
96
+ if (spec.blank === true)
97
+ return null; // empty state is valid
98
+ if (!spec.source || !SOURCES.includes(spec.source)) {
99
+ return `source must be one of ${SOURCES.join("|")}`;
100
+ }
101
+ // The DB browser is a distinct view: no viz/series/match. Only its optional deep-link
102
+ // fields are constrained (each must be a string when present).
103
+ if (spec.source === "db") {
104
+ for (const k of ["db", "table", "sql"]) {
105
+ if (spec[k] !== undefined && typeof spec[k] !== "string") {
106
+ return `${k} must be a string`;
107
+ }
108
+ }
109
+ return null;
110
+ }
111
+ if (spec.match !== undefined && !isRecord(spec.match)) {
112
+ return "match must be an object";
113
+ }
114
+ if (!Array.isArray(spec.viz) ||
115
+ spec.viz.length === 0 ||
116
+ spec.viz.some((v) => !VIZ.includes(v))) {
117
+ return `viz must be a non-empty subset of ${VIZ.join("|")}`;
118
+ }
119
+ if (spec.extract !== undefined && (!isRecord(spec.extract) || !spec.extract.field)) {
120
+ return "extract needs a field";
121
+ }
122
+ if (spec.alert !== undefined && !isRecord(spec.alert)) {
123
+ return "alert must be an object";
124
+ }
125
+ if (spec.bodyContains !== undefined && typeof spec.bodyContains !== "string") {
126
+ return "bodyContains must be a string";
127
+ }
128
+ if (spec.highlight !== undefined) {
129
+ const h = spec.highlight;
130
+ if (!isRecord(h) || !h.field || typeof h.value !== "number") {
131
+ return "highlight needs field + numeric value";
132
+ }
133
+ if (!HIGHLIGHT_OPS.includes((h.op ?? ">"))) {
134
+ return "highlight op invalid";
135
+ }
136
+ }
137
+ if (spec.viz.includes("graph")) {
138
+ if (!Array.isArray(spec.series) ||
139
+ spec.series.length < 1 ||
140
+ spec.series.length > 2) {
141
+ return "graph needs a series array of 1-2 entries";
142
+ }
143
+ for (const ser of spec.series) {
144
+ if (!ser || !SERIES_METRICS.includes(ser.metric)) {
145
+ return `series metric must be one of ${SERIES_METRICS.join("|")}`;
146
+ }
147
+ if (ser.metric === "avg" && !ser.field) {
148
+ return "avg series needs a field";
149
+ }
150
+ }
151
+ }
152
+ return null;
153
+ }
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@slack/radar-mcp",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "MCP server and web dashboard for on-device debugging of the Slack Android app via ADB",
5
5
  "type": "module",
6
6
  "main": "dist/mcp/index.js",
7
7
  "bin": {
8
8
  "radar-mcp": "dist/mcp/index.js",
9
9
  "slack-radar-mcp": "dist/mcp/index.js",
10
+ "slack-radar": "dist/mcp/index.js",
10
11
  "slack-radar-web": "dist/web/bin.js"
11
12
  },
12
13
  "files": [