@slack/radar-mcp 1.3.0 → 1.4.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,83 @@
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
+ * Scope note: this port covers the STREAMING sources only. The on-device SQLite
9
+ * browser (`source: "db"`) from the original PoC is intentionally omitted here and
10
+ * tracked as a separate change; `SOURCES` therefore does not include "db".
11
+ */
12
+ /** Streaming sources the dashboard can read. "all" is the unified timeline. */
13
+ export declare const SOURCES: readonly ["rtm", "network", "clog", "log", "all"];
14
+ export type Source = (typeof SOURCES)[number];
15
+ /** Widgets a spec may compose. */
16
+ export declare const VIZ: readonly ["counter", "rate", "sparkline", "feed", "inspector", "graph"];
17
+ export type Viz = (typeof VIZ)[number];
18
+ /** Metrics a graph series can plot. */
19
+ export declare const SERIES_METRICS: readonly ["ratePerSec", "count", "avg", "errorRate"];
20
+ export type SeriesMetric = (typeof SERIES_METRICS)[number];
21
+ declare const HIGHLIGHT_OPS: readonly [">", ">=", "<", "<=", "=="];
22
+ export type HighlightOp = (typeof HIGHLIGHT_OPS)[number];
23
+ export interface GraphSeries {
24
+ label?: string;
25
+ metric: SeriesMetric;
26
+ field?: string;
27
+ unit?: string;
28
+ }
29
+ export interface Highlight {
30
+ field: string;
31
+ op?: HighlightOp;
32
+ value: number;
33
+ label?: string;
34
+ }
35
+ export interface Extract {
36
+ kind?: Source;
37
+ parse?: "response_body" | "request_body" | "data";
38
+ field: string;
39
+ label?: string;
40
+ }
41
+ export interface Alert {
42
+ sound?: boolean;
43
+ flash?: boolean;
44
+ say?: string;
45
+ at?: number;
46
+ }
47
+ /**
48
+ * A dashboard spec. `blank: true` is the valid empty state (nothing rendered yet).
49
+ * `match` is a case-insensitive substring filter keyed on summary fields of the
50
+ * chosen source.
51
+ */
52
+ export interface Spec {
53
+ title?: string;
54
+ summary?: string;
55
+ blank?: boolean;
56
+ source?: Source;
57
+ match?: Record<string, string>;
58
+ bodyContains?: string;
59
+ viz?: Viz[];
60
+ rateWindowSec?: number;
61
+ series?: GraphSeries[];
62
+ extract?: Extract;
63
+ highlight?: Highlight;
64
+ alert?: Alert;
65
+ [key: string]: unknown;
66
+ }
67
+ /** The empty dashboard. Rendered until a spec is pushed. */
68
+ export declare const BLANK_SPEC: Spec;
69
+ /**
70
+ * Repair a spec into a renderable shape where the intent is recoverable, rather
71
+ * than rejecting it outright. The interpreter (and hand edits) sometimes emit
72
+ * `match` as a bare string instead of a `{field: value}` object, or a `graph`
73
+ * viz without a `series` array; coerce those into the natural form so the
74
+ * dashboard degrades gracefully. Returns the same object (mutated) for parity
75
+ * with the PoC; callers pass a throwaway parse result.
76
+ */
77
+ export declare function coerceSpec(input: unknown): Spec;
78
+ /**
79
+ * Validate a (coerced) spec. Returns null when valid, or a human-readable reason
80
+ * string when not. A bad spec must surface as an error, never as silent zeros.
81
+ */
82
+ export declare function validateSpec(s: unknown): string | null;
83
+ export {};
@@ -0,0 +1,135 @@
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
+ * Scope note: this port covers the STREAMING sources only. The on-device SQLite
9
+ * browser (`source: "db"`) from the original PoC is intentionally omitted here and
10
+ * tracked as a separate change; `SOURCES` therefore does not include "db".
11
+ */
12
+ /** Streaming sources the dashboard can read. "all" is the unified timeline. */
13
+ export const SOURCES = ["rtm", "network", "clog", "log", "all"];
14
+ /** Widgets a spec may compose. */
15
+ export const VIZ = [
16
+ "counter",
17
+ "rate",
18
+ "sparkline",
19
+ "feed",
20
+ "inspector",
21
+ "graph",
22
+ ];
23
+ /** Metrics a graph series can plot. */
24
+ export const SERIES_METRICS = [
25
+ "ratePerSec",
26
+ "count",
27
+ "avg",
28
+ "errorRate",
29
+ ];
30
+ const HIGHLIGHT_OPS = [">", ">=", "<", "<=", "=="];
31
+ /** The empty dashboard. Rendered until a spec is pushed. */
32
+ export const BLANK_SPEC = { title: "", blank: true, viz: [] };
33
+ function isRecord(v) {
34
+ return typeof v === "object" && v !== null && !Array.isArray(v);
35
+ }
36
+ /**
37
+ * Repair a spec into a renderable shape where the intent is recoverable, rather
38
+ * than rejecting it outright. The interpreter (and hand edits) sometimes emit
39
+ * `match` as a bare string instead of a `{field: value}` object, or a `graph`
40
+ * viz without a `series` array; coerce those into the natural form so the
41
+ * dashboard degrades gracefully. Returns the same object (mutated) for parity
42
+ * with the PoC; callers pass a throwaway parse result.
43
+ */
44
+ export function coerceSpec(input) {
45
+ if (!isRecord(input))
46
+ return BLANK_SPEC;
47
+ const s = input;
48
+ if (typeof s.match === "string") {
49
+ // "all" mixes types whose summary fields differ, so a single field guess
50
+ // would wrongly exclude other types; treat a bare-string match on "all"
51
+ // as no filter.
52
+ if (s.source === "all") {
53
+ delete s.match;
54
+ }
55
+ else {
56
+ const field = s.source === "rtm"
57
+ ? "event_type"
58
+ : s.source === "clog"
59
+ ? "event_name"
60
+ : "url";
61
+ s.match = { [field]: s.match };
62
+ }
63
+ }
64
+ // graph series repair: fill if missing/empty, trim to the validator's 1-2 cap,
65
+ // and drop entries with an unusable metric so the dashboard never hard-rejects.
66
+ if (Array.isArray(s.viz) && s.viz.includes("graph")) {
67
+ if (Array.isArray(s.series)) {
68
+ s.series = s.series.filter((x) => isRecord(x) &&
69
+ SERIES_METRICS.includes(x.metric) &&
70
+ !(x.metric === "avg" && !x.field));
71
+ if (s.series.length > 2)
72
+ s.series = s.series.slice(0, 2);
73
+ }
74
+ if (!Array.isArray(s.series) || s.series.length === 0) {
75
+ s.series = [{ label: "per second", metric: "ratePerSec", unit: "/s" }];
76
+ }
77
+ }
78
+ return s;
79
+ }
80
+ /**
81
+ * Validate a (coerced) spec. Returns null when valid, or a human-readable reason
82
+ * string when not. A bad spec must surface as an error, never as silent zeros.
83
+ */
84
+ export function validateSpec(s) {
85
+ if (!isRecord(s))
86
+ return "not an object";
87
+ const spec = s;
88
+ if (spec.blank === true)
89
+ return null; // empty state is valid
90
+ if (!spec.source || !SOURCES.includes(spec.source)) {
91
+ return `source must be one of ${SOURCES.join("|")}`;
92
+ }
93
+ if (spec.match !== undefined && !isRecord(spec.match)) {
94
+ return "match must be an object";
95
+ }
96
+ if (!Array.isArray(spec.viz) ||
97
+ spec.viz.length === 0 ||
98
+ spec.viz.some((v) => !VIZ.includes(v))) {
99
+ return `viz must be a non-empty subset of ${VIZ.join("|")}`;
100
+ }
101
+ if (spec.extract !== undefined && (!isRecord(spec.extract) || !spec.extract.field)) {
102
+ return "extract needs a field";
103
+ }
104
+ if (spec.alert !== undefined && !isRecord(spec.alert)) {
105
+ return "alert must be an object";
106
+ }
107
+ if (spec.bodyContains !== undefined && typeof spec.bodyContains !== "string") {
108
+ return "bodyContains must be a string";
109
+ }
110
+ if (spec.highlight !== undefined) {
111
+ const h = spec.highlight;
112
+ if (!isRecord(h) || !h.field || typeof h.value !== "number") {
113
+ return "highlight needs field + numeric value";
114
+ }
115
+ if (!HIGHLIGHT_OPS.includes((h.op ?? ">"))) {
116
+ return "highlight op invalid";
117
+ }
118
+ }
119
+ if (spec.viz.includes("graph")) {
120
+ if (!Array.isArray(spec.series) ||
121
+ spec.series.length < 1 ||
122
+ spec.series.length > 2) {
123
+ return "graph needs a series array of 1-2 entries";
124
+ }
125
+ for (const ser of spec.series) {
126
+ if (!ser || !SERIES_METRICS.includes(ser.metric)) {
127
+ return `series metric must be one of ${SERIES_METRICS.join("|")}`;
128
+ }
129
+ if (ser.metric === "avg" && !ser.field) {
130
+ return "avg series needs a field";
131
+ }
132
+ }
133
+ }
134
+ return null;
135
+ }
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@slack/radar-mcp",
3
- "version": "1.3.0",
3
+ "version": "1.4.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": [