@uptimizr/mcp 1.1.0 → 1.2.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.
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAa,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAsCvE;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,eAAe,GAAG,SAAS,CAqClE"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,sBAAsB,CAAC;AAqE9B;;;;;;;GAOG;AACH,MAAM,WAAW,sBAAsB;IACrC;;;;;;;;;;;;;;;;;;;OAmBG;IACH,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AAwBD;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAW9F;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,eAAe,EACvB,OAAO,GAAE,sBAA2B,GACnC,SAAS,CAiFX"}
package/dist/server.js CHANGED
@@ -1,12 +1,11 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- import { z } from "zod";
3
- import { readTools } from "@uptimizr/agent-core";
2
+ import { rawTools, readTools, writeTools, } from "@uptimizr/agent-core";
4
3
  import { registerResources } from "./resources.js";
5
4
  import { registerPrompts } from "./prompts.js";
6
5
  import { version } from "./version.js";
7
6
  /**
8
- * Normalise a collector response into the `{ rows }` envelope every tool's
9
- * `outputSchema` declares. Aggregate endpoints already return an array; the
7
+ * Normalise a collector response into the `{ rows }` envelope a `format=full`
8
+ * result is reported in. Aggregate endpoints already return an array; the
10
9
  * single-object reads (a session descriptor, a scene representation, a one-row
11
10
  * summary) become a one-element array so a client can treat every tool's
12
11
  * structured result the same way. A `204`-style empty body becomes no rows.
@@ -19,8 +18,36 @@ function toRows(data) {
19
18
  return [data];
20
19
  }
21
20
  /**
22
- * Validate the rows against the tool's registry-derived output schema and
23
- * return the **parsed** result as the structured payload.
21
+ * Whether the collector really answered with the envelope the call asked for:
22
+ * `format=table` with a `meta` block around its rows, `format=summary` with a
23
+ * `kind`-tagged digest and its `reading`. Both keys are required so a *row* that
24
+ * happens to have a `kind` column is never mistaken for a digest.
25
+ */
26
+ function isEnvelope(data, format) {
27
+ if (data == null || typeof data !== "object" || Array.isArray(data))
28
+ return false;
29
+ const payload = data;
30
+ if (format === "table")
31
+ return "meta" in payload && "rows" in payload;
32
+ if (format === "summary")
33
+ return "kind" in payload && "reading" in payload;
34
+ return false;
35
+ }
36
+ /**
37
+ * The structured payload for one tool result: the collector's response in the
38
+ * envelope the call asked for, validated against the tool's registry-derived
39
+ * output schema and returned **parsed**.
40
+ *
41
+ * `format=table` and `format=summary` are returned as they arrive — the whole
42
+ * point of those envelopes is the `meta` / `reading` they carry, and stripping
43
+ * them here is precisely the bug that made the SDK reject the recommended path
44
+ * with `-32602` (#350). `full` keeps the `{ rows }` wrapping it has always had.
45
+ * Which envelope to expect is decided by the `format` the request actually
46
+ * carried — not guessed from the payload — and the payload must then actually
47
+ * *be* that envelope (`meta` + `rows`, or a `kind`-tagged digest). Anything
48
+ * else is wrapped as rows, which is what keeps a modern client working against
49
+ * an **older collector** that does not know `format` and answers a `table`
50
+ * request with the bare rows (or the bare record) it always returned.
24
51
  *
25
52
  * The registry's numeric columns are strict `z.number()`: since ADR 0051 §2 the
26
53
  * *collector* guarantees numbers, coercing each dialect's wire format
@@ -28,32 +55,106 @@ function toRows(data) {
28
55
  * point rows leave its driver. So this is a check, not a repair — the advertised
29
56
  * schema describes the API, and normalising here would hide a store regression.
30
57
  *
31
- * If the rows do not match the schema the raw rows are passed through; the SDK's
32
- * own output validation then reports the offending column by name, which is the
33
- * honest outcome for a collector that is out of contract.
58
+ * If the payload does not match the schema it is passed through unchanged; the
59
+ * SDK's own output validation then reports the offending column by name, which
60
+ * is the honest outcome for a collector that is out of contract.
61
+ */
62
+ function structuredResult(tool, data, format) {
63
+ // A generated per-metric tool returns one metric's rows, so the `format`
64
+ // envelope above is the default. The `query` tool (ADR 0051 §3) chooses its
65
+ // own shape — what comes back depends on the `format` that was asked for —
66
+ // so it supplies the wrapper itself.
67
+ const payload = tool.structuredContent?.(data) ?? (isEnvelope(data, format) ? data : { rows: toRows(data) });
68
+ const parsed = tool.outputSchema.safeParse(payload);
69
+ return parsed.success ? parsed.data : payload;
70
+ }
71
+ /**
72
+ * Server-level `instructions` for a session whose key capabilities are known.
73
+ *
74
+ * Only built when {@link CreateMcpServerOptions.capabilities} is supplied, so
75
+ * the stdio server's `initialize` result stays byte-for-byte what it has always
76
+ * been. Capability names are not secret — `GET /api/v1/whoami` returns the same
77
+ * list to the key's holder — and naming them saves an agent a round of
78
+ * trial-and-error against tools it could never call.
79
+ */
80
+ function instructionsFor(capabilities) {
81
+ const granted = capabilities.length > 0 ? capabilities.join(", ") : "none";
82
+ return ("Every tool here reads one aggregate metric from the connected Uptimizr collector, " +
83
+ "always scoped to the project the API key belongs to — no cross-project access, and no " +
84
+ "personally identifying data. " +
85
+ `The key this session is bound to holds these capabilities: ${granted}. ` +
86
+ "Tools outside that set are not registered, and the collector refuses them independently. " +
87
+ "Read the uptimizr://capabilities resource first: it gives every metric's grain, column " +
88
+ "units, row limits, interpretation and caveats, so a query can be planned rather than guessed.");
89
+ }
90
+ /**
91
+ * Ask the collector what the configured key may do (`GET /api/v1/whoami`).
92
+ *
93
+ * Never throws: an older collector without the route, an unreachable one, or a
94
+ * key that cannot even read all yield an empty capability set, and the server
95
+ * then starts read-only rather than not starting at all. A degraded but useful
96
+ * server beats no server.
34
97
  */
35
- function structuredRows(outputSchema, data) {
36
- const rows = toRows(data);
37
- const parsed = z.object(outputSchema).safeParse({ rows });
38
- return parsed.success ? parsed.data : { rows };
98
+ export async function fetchKeyCapabilities(client) {
99
+ try {
100
+ const whoami = (await client.get("/api/v1/whoami"));
101
+ return Array.isArray(whoami?.capabilities)
102
+ ? whoami.capabilities.filter((capability) => typeof capability === "string")
103
+ : [];
104
+ }
105
+ catch {
106
+ return [];
107
+ }
39
108
  }
40
109
  /**
41
- * Build the Uptimizr MCP server: a read-only `McpServer` whose tools each wrap
42
- * one collector query endpoint via the injected `CollectorClient`. The server
43
- * holds no business logic — it forwards validated arguments and returns the
44
- * collector's JSON (ADR 0005 / ADR 0017). Alongside the tools it exposes
110
+ * Build the Uptimizr MCP server: an `McpServer` whose tools each wrap one
111
+ * collector endpoint via the injected `CollectorClient`. The server holds no
112
+ * business logic — it forwards validated arguments and returns the collector's
113
+ * JSON (ADR 0005 / ADR 0017). Alongside the tools it exposes
45
114
  * capability-discovery **resources** and curated analysis **prompts** so agents
46
115
  * can self-orient (ADR 0050 §7).
47
116
  *
48
- * The tool catalog is generated from the `@uptimizr/db` metric registry
117
+ * The **analytics** tool catalog is generated from the metric registry
49
118
  * (ADR 0051 §1), so `tools/list` covers every metric the collector serves on an
50
119
  * endpoint. Each tool advertises the registry-derived `outputSchema` and returns
51
- * both `structuredContent` (the typed `{ rows }` envelope) and the `content`
52
- * text a client without structured-output support still reads.
120
+ * both `structuredContent` — the `format` envelope the call asked for, which
121
+ * since #336 is `{ meta, rows }` by default — and the `content` text a client
122
+ * without structured-output support still reads. That catalog is entirely
123
+ * read-only: **events cannot be written, altered or deleted through this
124
+ * server** (ADR 0051 §9).
125
+ *
126
+ * `options.capabilities` is what the **key** behind `client` is allowed to do
127
+ * (`GET /api/v1/whoami`). Tools whose endpoint needs more than the ordinary
128
+ * `query` capability are registered only when the key really holds it, so
129
+ * `tools/list` describes what this session can actually do rather than what the
130
+ * collector could do for somebody else: the raw-session tools of #314 behind
131
+ * `query:raw`, the **metadata** tools of #310 — annotations, glossary, saved
132
+ * analyses — behind `annotate`. Omit it and only the `query` surface is served,
133
+ * the safe default for a caller that has not looked the key up.
134
+ *
135
+ * The factory is **transport-agnostic**: `bin.ts` connects it to stdio, and the
136
+ * collector connects one instance per authenticated Streamable HTTP session at
137
+ * `/mcp` (ADR 0051 §7), passing that session's key capabilities through
138
+ * `options`. Both get the same tools, resources and prompts from this one place.
53
139
  */
54
- export function createMcpServer(client) {
55
- const server = new McpServer({ name: "uptimizr-mcp", version }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
56
- for (const tool of readTools) {
140
+ export function createMcpServer(client, options = {}) {
141
+ // The default is the ordinary read surface, never "nothing": a caller that has
142
+ // not looked the key up still gets every `query` tool, exactly as before.
143
+ // `instructions`, by contrast, keys off the *supplied* set, so a stdio server
144
+ // built without one keeps the `initialize` result it has always had.
145
+ const capabilities = options.capabilities ?? ["query"];
146
+ const server = new McpServer({ name: "uptimizr-mcp", version }, {
147
+ capabilities: { tools: {}, resources: {}, prompts: {} },
148
+ ...(options.capabilities ? { instructions: instructionsFor(options.capabilities) } : {}),
149
+ });
150
+ const tools = [
151
+ ...readTools,
152
+ // `query:raw` additionally requires `ENABLE_RAW_SESSION_RETENTION` on the
153
+ // collector (ADR 0003), which this process cannot see — a narrative tool on
154
+ // a retention-disabled collector still answers 403, and says so.
155
+ ...(capabilities.includes("query:raw") ? rawTools : []),
156
+ ];
157
+ for (const tool of tools) {
57
158
  server.registerTool(tool.name, {
58
159
  title: tool.title,
59
160
  description: tool.description,
@@ -68,7 +169,7 @@ export function createMcpServer(client) {
68
169
  return { content: [{ type: "text", text }] };
69
170
  return {
70
171
  content: [{ type: "text", text }],
71
- structuredContent: structuredRows(tool.outputSchema, data),
172
+ structuredContent: structuredResult(tool, data, params.format),
72
173
  };
73
174
  }
74
175
  catch (err) {
@@ -77,7 +178,31 @@ export function createMcpServer(client) {
77
178
  }
78
179
  });
79
180
  }
80
- registerResources(server, client);
181
+ // Metadata tools (#310, ADR 0051 §5): annotations, glossary, saved analyses.
182
+ //
183
+ // They appear in `tools/list` only for a key that holds `annotate`. That is a
184
+ // usability decision, not the security boundary — the collector refuses the
185
+ // write either way with a 403 — but offering an agent a tool it will always
186
+ // be refused for wastes its context and its patience.
187
+ //
188
+ // Nothing here can write, alter or delete an event: the tools call the three
189
+ // metadata endpoints and nothing else, and every call is audited by the
190
+ // collector (ADR 0051 §7/§9).
191
+ if (capabilities.includes("annotate")) {
192
+ for (const tool of writeTools) {
193
+ server.registerTool(tool.name, { title: tool.title, description: tool.description, inputSchema: tool.inputSchema }, async (args) => {
194
+ try {
195
+ const data = await tool.execute(client, args);
196
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
197
+ }
198
+ catch (err) {
199
+ const message = err instanceof Error ? err.message : String(err);
200
+ return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
201
+ }
202
+ });
203
+ }
204
+ }
205
+ registerResources(server, client, { capabilities });
81
206
  registerPrompts(server);
82
207
  return server;
83
208
  }
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
package/llms.txt CHANGED
@@ -1,7 +1,9 @@
1
1
  # @uptimizr/mcp
2
2
 
3
- > A read-only Model Context Protocol server over an Uptimizr collector's query API. Lets an agent
4
- > query a consumer's own 3D analytics; thin wrapper, GET-only, no data egress.
3
+ > A Model Context Protocol server over an Uptimizr collector's API. Lets an agent query a consumer's
4
+ > own 3D analytics; thin wrapper, no data egress. Events are read-only; project-metadata writes
5
+ > (annotations, glossary, saved analyses, pinned panels) are registered only for a key holding
6
+ > `annotate`.
5
7
 
6
8
  ## Docs
7
9
 
@@ -10,7 +12,7 @@
10
12
  - [Integration & API reference](https://github.com/RaananW/Uptimizr/blob/main/docs/integration.md): the underlying query endpoints.
11
13
  - [Architecture Decision Records](https://github.com/RaananW/Uptimizr/tree/main/docs/adr): privacy model (0003), thin backends (0005), consumer-facing agents (0017).
12
14
 
13
- ## Tools (read-only)
15
+ ## Analytics tools (read-only)
14
16
 
15
17
  <!-- generated:registry-tool-names:start — generated by `pnpm gen:docs`; edit the metric registry, not this list -->
16
18
 
@@ -20,23 +22,196 @@
20
22
  `position_heatmap`, `session_trajectory`, `aggregate_paths`, `scene_coverage`, `camera_distance`,
21
23
  `click_rays`, `flow_links`, `top_meshes`, `mesh_sources`, `mesh_trend`, `mesh_dwell`,
22
24
  `mesh_blind_spots`, `mesh_interaction_kinds`, `mesh_reachability`, `dead_clicks`, `rage_clicks`,
23
- `hover_dwell`, `interaction_sources`, `top_input_actions`, `camera_gestures`, `navigation_stats`,
24
- `backtrack_ratio`, `perf_summary`, `render_scale_truth`, `perf_distribution`, `fps_histogram`,
25
- `frame_time_percentiles`, `jank_rate`, `perf_churn`, `perf_by_device`, `perf_by_scene`,
26
- `perf_heatmap`, `compile_stalls`, `resource_summary`, `resource_percentiles`, `stability_counts`,
27
- `graphics_diagnostics`, `error_heatmap`, `rendering_technology`, `capability_changes`,
28
- `xr_rotation`, `xr_sources`, `xr_abandonment`, `xr_locomotion`, `xr_tracking_quality`,
29
- `boundary_heatmap`, `boundary_heatmap_stats`, `xr_boundary_contacts`,
30
- `ar_placement_time_to_place`, `ar_placement_attempts`, `ar_placement_surfaces`, `funnel`,
31
- `scene_retention`, `load_bounce_funnel`, `variant_leaderboard`
25
+ `hover_dwell`, `interaction_sources`, `top_input_actions`, `custom_event_vocabulary`,
26
+ `camera_gestures`, `navigation_stats`, `backtrack_ratio`, `perf_summary`, `render_scale_truth`,
27
+ `perf_distribution`, `fps_histogram`, `frame_time_percentiles`, `jank_rate`, `perf_churn`,
28
+ `perf_by_device`, `perf_by_scene`, `perf_heatmap`, `compile_stalls`, `resource_summary`,
29
+ `resource_percentiles`, `stability_counts`, `graphics_diagnostics`, `error_heatmap`,
30
+ `rendering_technology`, `capability_changes`, `xr_rotation`, `xr_sources`, `xr_abandonment`,
31
+ `xr_locomotion`, `xr_tracking_quality`, `boundary_heatmap`, `boundary_heatmap_stats`,
32
+ `xr_boundary_contacts`, `ar_placement_time_to_place`, `ar_placement_attempts`,
33
+ `ar_placement_surfaces`, `funnel`, `scene_retention`, `load_bounce_funnel`, `variant_leaderboard`,
34
+ `insight_baseline`, `insight_movers`, `insight_anomalies`, `insight_significance`,
35
+ `insight_scene_health`
36
+
37
+ Only on a key holding `query:raw`, and only when the collector runs with
38
+ `ENABLE_RAW_SESSION_RETENTION` (ADR 0003):
39
+
40
+ `session_narrative`
32
41
 
33
42
  <!-- generated:registry-tool-names:end -->
34
43
 
44
+ ## Insight primitives (ADR 0051 §4)
45
+
46
+ Five of the tools are readings *about* the other metrics, and they are the right first call on an
47
+ open-ended question — "how are things?" does not mean "call thirty tools".
48
+
49
+ - `insight_scene_health` — one 0-100 score per scene over six weighted factors (perf stability,
50
+ jank, errors, dead clicks, coverage, XR abandonment), least healthy first. The right *unscoped*
51
+ starting point: it answers which scene to open. Every factor carries the `metric` id behind it,
52
+ its `raw` value and the project `baseline` it was compared with, so the next call and the
53
+ sentence to write are both already in the row. **50 is the project norm, not a pass mark** — the
54
+ score compares a scene with the rest of the project's recent past — and a factor with
55
+ `score: null` was not counted (its `note` says why).
56
+ - `insight_movers` — what changed against a reference window (the previous equal one by default),
57
+ for every comparable metric in scope, ranked by a robust z-score: the change divided by how much
58
+ that metric normally swings, so a noisy metric must move much further than a steady one to reach
59
+ the top. Bounded: at most 24 metrics are scanned per request.
60
+ - `insight_baseline` — what is normal for one metric in one scene: `median` and `mad`, the
61
+ `p10`..`p90` band, and the drift `slope`. Call it on whatever moved, to say whether the new level
62
+ is actually outside normal rather than merely different.
63
+ - `insight_anomalies` — *when* one metric went wrong. One row per bucket that does not belong in
64
+ the series: `kind: "spike" | "drop"` for a bucket more than `sensitivity` (default 3) standard
65
+ deviations from the trailing window, and `kind: "shift"` at the bucket where the level moved and
66
+ stayed moved. Where the metric declares a dimension it can be split by, `contributor` names the
67
+ mesh, source, input action, event type or scene holding the largest `share` of the excess.
68
+ Bounded: one scan for the series plus at most three attribution scans per request.
69
+ - `insight_significance` — whether a difference between two windows could be chance: `effect`,
70
+ `ci95`, `p`, and the `test` that produced them (a two-proportion z with Wilson intervals for a
71
+ declared rate, Welch's t over the per-bucket values for a level, an exact Poisson rate test for a
72
+ bare count — picked from the measure, not from you). **Read `ci95` before `p`**: an interval that
73
+ straddles 0 means you cannot tell yet, and `powerNote` says what this much data could have
74
+ detected at all. Welch's `n` is the number of **buckets**, not events. It compares two *windows*,
75
+ not two segments: a variant-versus-variant question is a `400`, not a wrong answer.
76
+
77
+ Read `insight_anomalies`' `z` as standard deviations and `insight_movers`' as the same ratio
78
+ unscaled — they differ by a constant factor (1.4826) and must not be compared directly. A sustained
79
+ level change is reported both as a `shift` and, for the days right after it, as `drop`/`spike`
80
+ rows: two true statements about one event.
81
+
82
+ Two fields decide whether a mover is worth reporting, and both are easy to misread:
83
+
84
+ - `direction` is the registry's opinion of what a **rise** in that metric means — `up` (good),
85
+ `down` (bad) or `neutral`. It is *not* the direction of the move: read it with the sign of
86
+ `delta`, so a rise in a `down` metric (errors, dead clicks, jank) is a regression.
87
+ - `aboveMinSample: false` means the delta is real arithmetic but the denominator is below the
88
+ metric's declared minimum. Such rows are returned rather than dropped — "we cannot tell" and
89
+ "nothing changed" are different answers — and must never be reported as findings.
90
+
91
+ Statistics are computed in pure TypeScript over portable day/hour buckets, so the same data yields
92
+ the same answer on every storage engine. Only `comparable` metrics with a faithful per-bucket form
93
+ participate; asking for one that has none returns `400` naming every id that does.
94
+
95
+ ## Result formats (`format`)
96
+
97
+ Every aggregate tool takes `format` — the envelope its rows arrive in, not a filter (ADR 0051 §2).
98
+ Omit it and you get `table`: the tools' own default, sent explicitly, so the HTTP endpoints still
99
+ default to `full` for every other client. All three validate against the tool's `outputSchema`.
100
+
101
+ - `summary` — **prefer this.** A bounded digest: `ranked` top rows, a `series` trend, merged spatial
102
+ `clusters`, or a single `record`, with shares, a sample size, the metric's `caveats` and a
103
+ templated `reading` sentence. Capped at the metric's `limits.maxSummaryRows`, so a 500-bin heatmap
104
+ costs the same as a 5-bin one. `reading` and `caveats` are templated by pure code — no model —
105
+ so identical rows always produce identical words.
106
+ - `table` — **the default.** `{ meta, rows }`: every row plus the metric, range, applied filters,
107
+ sample size, row count, a truncation flag and the registry limits. Self-describing, but not
108
+ bounded — the rows are still all of them, so ask for `summary` when the answer could be large.
109
+ - `full` — the bare rows, unchanged, with no envelope at all.
110
+
111
+ Shares appear only where the measure can honestly be summed (an FPS or ratio metric reports
112
+ `total: null` and no shares). Cluster coordinates are grid indices — multiply by the effective
113
+ `cellSize` for world space. When the scene has a registered proxy and named regions, a world-space
114
+ cluster is also labelled with `region` (the smallest containing region), `regions[]` (all containing
115
+ ids), `nearestMesh` and `distance` (world units, `0` when the mesh box contains it), and
116
+ `drill.region` becomes that region id — pass it straight back as the `region` argument. Quote the
117
+ label rather than the coordinate; a `null` means the scene registered nothing that could answer (the
118
+ `caveats` say which), so do not invent a landmark. `session_meta` and `scene_representation` are
119
+ stored records, not aggregations, and take no `format`.
120
+
121
+ ## Resources and prompts
122
+
123
+ - `uptimizr://context` (`application/json`) — the live project context document
124
+ (`GET /api/v1/context`, ADR 0051 §5): scene ids + labels + named region ids, the custom-event
125
+ vocabulary with its `props` keys and coarse types, top meshes, bound input actions, data freshness,
126
+ retention flags, store engine, glossary, recent annotations, and `metrics.disabledByCapture` —
127
+ metrics that return empty because their capture channel is off. Bounded and cached per project.
128
+ **Read it first**, and use the ids and names it gives you instead of inferring your own.
129
+ - `uptimizr://capabilities` (`application/json`) — static descriptor: schema version, canonical
130
+ event types, the tool catalog, the parameter-semantics glossary, and `metrics` (the whole registry
131
+ with grain, column units, row JSON Schema, filters, limits, interpretation, caveats, source
132
+ channels). No collector call. Read it after the context.
133
+ - `uptimizr://scenes` (`application/json`) — the live scene ids with recent activity, i.e. the valid
134
+ values for the `scene` parameter. Fetched through the read-only query API.
135
+ - `uptimizr://skills` (`application/json`) — the packaged methodology skills below: name, title,
136
+ description, the tools each method relies on, the capabilities it needs and the arguments it
137
+ takes. Served from the package; no collector call.
138
+ - Prompts: one per packaged skill (`attention_hotspots`, `conversion_investigation`,
139
+ `performance_regression_triage`, `weekly_scene_health`, `xr_comfort_audit`) — each opens by
140
+ telling the agent to read `uptimizr://context` first, then renders one user message sequencing
141
+ the read-only tools; they fetch nothing themselves.
142
+
143
+ ## Packaged methodology skills (ADR 0051 §7)
144
+
145
+ Every prompt template is a packaged skill: an Agent Skills file under `skills/<name>/SKILL.md` in
146
+ this tarball, whose body is the investigation as numbered steps. The catalog is also the
147
+ `uptimizr://skills` resource; `prompts/get` returns the rendered method.
148
+
149
+ <!-- generated:registry-skill-names:start — generated by `pnpm gen:docs`; edit the SKILL.md files, not this list -->
150
+
151
+ - `attention_hotspots` (scene (required), range) — Find where visitors look and click in a scene: view-direction concentration, gaze→mesh flow, the objects that draw the most interaction, and the ones nobody ever notices. USE FOR: deciding where to put a call to action, finding ignored or invisible content, explaining why an object gets no clicks, laying out a scene around what people actually look at.
152
+ Method: `skills/attention-hotspots/SKILL.md`. Tools: `camera_heatmap`, `flow_links`, `click_rays`, `top_meshes`, `mesh_dwell`, `mesh_blind_spots`, `query`.
153
+ - `conversion_investigation` (scene, range) — Find out where a funnel loses people and whether the loss is real: step-by-step drop-off, the bounce that happens before the funnel even starts, scene-to-scene retention, variant performance, and the interaction failures (dead clicks, rage clicks, unreachable meshes) that explain a stalled step. USE FOR: a funnel that converts worse than expected, an A/B variant comparison, "where do people drop off", diagnosing a step nobody completes.
154
+ Method: `skills/conversion-investigation/SKILL.md`. Tools: `funnel`, `load_bounce_funnel`, `scene_retention`, `variant_leaderboard`, `dead_clicks`, `rage_clicks`, `mesh_reachability`, `flow_links`, `insight_significance`, `insight_movers`, `query`.
155
+ - `performance_regression_triage` (scene, range) — Triage a frame-rate or stability regression: confirm it moved, date it, locate it (which scene, device class, place in the scene), and name the mechanism — jank, shader compile stalls, memory pressure, a render-scale change or a rendering-technology shift. USE FOR: "the app got slower", a FPS drop after a release, stutter reports, deciding whether a regression is real or noise.
156
+ Method: `skills/performance-regression-triage/SKILL.md`. Tools: `insight_movers`, `insight_anomalies`, `insight_significance`, `insight_baseline`, `perf_summary`, `perf_distribution`, `frame_time_percentiles`, `jank_rate`, `perf_by_device`, `perf_by_scene`, `perf_heatmap`, `compile_stalls`, `resource_percentiles`, `render_scale_truth`, `rendering_technology`, `query`.
157
+ - `weekly_scene_health` (scene, range) — A weekly health check for a scene (or the whole project): a weighted health score with every factor traced back to the metric behind it, what changed against last week, traffic, event mix, performance, and the most-interacted meshes. USE FOR: the recurring "how is the scene doing?" review, a scheduled weekly or monthly report, a first look at a project you do not know yet, deciding which scene to investigate next.
158
+ Method: `skills/weekly-scene-health/SKILL.md`. Tools: `insight_scene_health`, `insight_movers`, `insight_baseline`, `insight_significance`, `insight_anomalies`, `event_counts`, `timeseries`, `perf_summary`, `top_meshes`, `list_sessions`, `query`.
159
+ - `xr_comfort_audit` (scene, range) — Audit VR/AR comfort for a scene (or the whole project): rapid head rotation, locomotion style, tracking quality, guardian/boundary contacts, input-source mix, and the short sessions that mean someone took the headset off. USE FOR: motion-sickness complaints, immersive sessions that end early, choosing a locomotion scheme, checking whether a play space is big enough.
160
+ Method: `skills/xr-comfort-audit/SKILL.md`. Tools: `xr_rotation`, `xr_locomotion`, `xr_abandonment`, `xr_sources`, `xr_tracking_quality`, `xr_boundary_contacts`, `boundary_heatmap_stats`, `insight_scene_health`, `insight_movers`, `query`.
161
+
162
+ <!-- generated:registry-skill-names:end -->
163
+
164
+ ## Transports
165
+
166
+ Two transports, one server: `createMcpServer()` is transport-agnostic.
167
+
168
+ - **stdio** — `npx @uptimizr/mcp`, launched by a desktop client. What this package's `bin` does.
169
+ - **Collector-hosted Streamable HTTP** — the collector serves the same server at `POST/GET/DELETE
170
+ /mcp` when it is started with `COLLECTOR_MCP_HTTP=1` (ADR 0051 §7). A remote client connects with
171
+ a URL plus `x-api-key` (or `Authorization: Bearer <key>`); tool calls are audited with
172
+ `surface: "mcp-http"`. The catalog, resources and prompts are identical to stdio's.
173
+
174
+ ## Required key capability
175
+
176
+ `UPTIMIZR_API_KEY` needs the `query` capability
177
+ (`uptimizr new-key <projectId> --capabilities query --label "mcp-agent"`; `query` is the default).
178
+ Add `annotate` only to let the agent write project metadata — the server calls
179
+ `GET /api/v1/whoami` once at start-up and registers the metadata tools only when the key holds it.
180
+ `query:raw` is **optional and off by default**: the default catalog is aggregate reads only, and a
181
+ key that holds it gains exactly one more tool, `session_narrative` (see below). `ingest` is never
182
+ used. A key missing a required capability is refused with `403`.
183
+
35
184
  ## Key exports
36
185
 
37
- - `createMcpServer(client)` — build the read-only MCP server.
186
+ - `createMcpServer(client, options?)` — build the MCP server. `options.capabilities` is the bound
187
+ key's capability set, passed by the collector-hosted transport (or read from `/api/v1/whoami` by
188
+ the stdio bin) so a session's surface matches its key: metadata write tools are registered only
189
+ when it includes `annotate`, and omitting it registers the read catalog alone.
190
+ - `fetchKeyCapabilities(client)` — ask `GET /api/v1/whoami` what the key holds (never throws).
38
191
  - `createCollectorClient(config)` — the thin GET-only collector client.
39
192
  - `readMcpConfig()` — read `UPTIMIZR_COLLECTOR_URL` + `UPTIMIZR_API_KEY` from the environment.
40
193
  - `readTools` — the read-only tool catalog (one entry per query endpoint), generated from the
41
- `@uptimizr/metrics` metric registry: 69 tools, each with an input schema, an MCP `outputSchema` and the
42
- metric's interpretation notes and caveats in its description (ADR 0051).
194
+ `@uptimizr/metrics` metric registry plus `query` and `list_subscriptions`: 77 tools, each with an
195
+ input schema, an MCP `outputSchema` covering all three `format` envelopes, and the metric's
196
+ interpretation notes and caveats in its description (ADR 0051).
197
+ - `writeTools` — the project-metadata tools (`annotate`, `define_term`, `save_analysis`,
198
+ `pin_panel`, `unpin_panel`, `list_annotations`, `list_glossary`, `list_analyses`,
199
+ `list_panels`). A separate export from `readTools` on
200
+ purpose, so an integration's read-only stance stays inspectable. They write metadata only — no
201
+ event can be written, altered or deleted through them. `pin_panel` stores a panel **spec** — a
202
+ metric id, a chart name, some column names — that the dashboard draws with panels it already
203
+ ships; send the `query` document with `range: "inherit"` and a chart the metric's grain supports,
204
+ or it is a `400` naming the charts that would have worked. `unpin_panel` removes a panel for
205
+ everyone on the project. At most 50 pinned panels per project (`409` past the cap).
206
+
207
+ ## Capability-gated tools (ADR 0051 §7)
208
+
209
+ - The default catalog is the `query` surface: aggregate reads only, no raw per-session tool.
210
+ - `createMcpServer(client, { capabilities })` takes the key's capability set (from `GET /api/v1/whoami`). When it includes `query:raw`, the server also registers `session_narrative` — an ordered, bounded account of what one session did, gated on the collector also running with `ENABLE_RAW_SESSION_RETENTION`. Omit the option and only the `query` surface is served.
211
+ - The `uptimizr-mcp` binary discovers the capability set at start-up (best effort; a failure falls back to the `query` surface).
212
+
213
+ ## Non-metric reads
214
+
215
+ - `list_subscriptions` — the project's conditional subscriptions (ADR 0051 §6): what the
216
+ collector is watching for and how each one last went. No arguments. Read-only; creating or
217
+ deleting one needs `annotate` and is not exposed as a tool.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uptimizr/mcp",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Read-only Model Context Protocol (MCP) server over an Uptimizr collector's query API — let an agent ask questions of your own 3D analytics, on your own infrastructure.",
5
5
  "keywords": [
6
6
  "uptimizr",
@@ -40,6 +40,7 @@
40
40
  },
41
41
  "files": [
42
42
  "dist",
43
+ "skills",
43
44
  "README.md",
44
45
  "LICENSE",
45
46
  "AGENTS.md",
@@ -48,27 +49,27 @@
48
49
  "sideEffects": false,
49
50
  "dependencies": {
50
51
  "@modelcontextprotocol/sdk": "^1.30.0",
51
- "zod": "^4.5.4",
52
- "@uptimizr/agent-core": "1.1.0",
53
- "@uptimizr/metrics": "0.1.0",
54
- "@uptimizr/schema": "1.1.0"
52
+ "zod": "^4.6.5",
53
+ "@uptimizr/agent-core": "1.2.0",
54
+ "@uptimizr/metrics": "0.2.0",
55
+ "@uptimizr/schema": "1.2.0"
55
56
  },
56
57
  "devDependencies": {
57
- "@types/node": "^26.4.1",
58
- "tsx": "^4.23.13",
59
- "vitest": "^4.1.11"
58
+ "@types/node": "^26.6.2",
59
+ "tsx": "^4.23.15",
60
+ "vitest": "^5.0.1"
60
61
  },
61
62
  "engines": {
62
63
  "node": ">=22"
63
64
  },
64
65
  "scripts": {
65
- "build": "tsc -p tsconfig.json",
66
+ "build": "tsc -p tsconfig.json && node ../../../scripts/copy-dir.mjs ../agent-core/skills skills",
66
67
  "dev": "tsc -p tsconfig.json --watch",
67
68
  "serve": "tsx src/bin.ts",
68
69
  "start": "node dist/bin.js",
69
70
  "typecheck": "tsc -p tsconfig.json --noEmit",
70
71
  "test": "vitest run",
71
72
  "lint": "eslint .",
72
- "clean": "rimraf -g dist *.tsbuildinfo"
73
+ "clean": "rimraf -g dist skills *.tsbuildinfo"
73
74
  }
74
75
  }
@@ -0,0 +1,88 @@
1
+ ---
2
+ name: attention_hotspots
3
+ title: Attention hot-spots for a scene
4
+ description: >-
5
+ Find where visitors look and click in a scene: view-direction concentration, gaze→mesh flow, the
6
+ objects that draw the most interaction, and the ones nobody ever notices. USE FOR: deciding where
7
+ to put a call to action, finding ignored or invisible content, explaining why an object gets no
8
+ clicks, laying out a scene around what people actually look at. Trigger phrases: what do people
9
+ look at, attention hotspots, where do visitors click, which meshes get ignored, blind spots,
10
+ gaze heatmap, is anyone seeing this object.
11
+ tools:
12
+ - camera_heatmap
13
+ - flow_links
14
+ - click_rays
15
+ - top_meshes
16
+ - mesh_dwell
17
+ - mesh_blind_spots
18
+ - query
19
+ capabilities:
20
+ - query
21
+ args:
22
+ - name: scene
23
+ required: true
24
+ description: The scene id to analyse (see the uptimizr://scenes resource).
25
+ - name: range
26
+ required: false
27
+ default: the last 7 days
28
+ description: The window to analyse, in words — e.g. "the last 7 days", "since launch".
29
+ ---
30
+
31
+ Where does attention concentrate in scene "{{scene}}" over {{range}}?
32
+
33
+ Work through the method below with the read-only tools it names — all of them scoped with
34
+ `scene="{{scene}}"` — and synthesise one answer.
35
+
36
+ 1. **Orient before you ask anything.** Read the `uptimizr://context` resource first: it gives the
37
+ real scene ids, the scene's **named regions** and the custom-event names this project emits, and
38
+ it tells you which metrics are empty because their capture channel is off. Name regions the way
39
+ the project names them — "the checkout counter", not "the cluster at x≈3".
40
+
41
+ 2. **Where do they look?** `camera_heatmap` (`scene="{{scene}}"`) gives the view-direction
42
+ distribution — what people point the camera at, whether or not they ever click it. Ask for
43
+ `format: "summary"`: the digest merges neighbouring cells into a handful of clusters with a
44
+ share each and a plain-language `reading`, which is what you want here; the raw grid is
45
+ thousands of cells you cannot describe.
46
+
47
+ 3. **Does looking turn into touching?** `flow_links` (`scene="{{scene}}"`) links where the gaze was
48
+ to the mesh that was then clicked. A strong link is a working call to action; a heavy look with
49
+ no outgoing link is content that draws the eye and then disappoints.
50
+
51
+ 4. **Where do the clicks land?** `click_rays` (`scene="{{scene}}"`) gives view-gated clicks per
52
+ voxel and mesh — clicks attributed to what the visitor could actually see, not to whatever the
53
+ ray happened to pass through.
54
+
55
+ 5. **Rank the objects.** `top_meshes` (`scene="{{scene}}"`) for the most-interacted meshes, and
56
+ `mesh_dwell` (`scene="{{scene}}"`) for how long attention rests on each one. Dwell without
57
+ interaction is hesitation, and it usually means the object looks clickable and is not, or is
58
+ clickable and does not look it.
59
+
60
+ 6. **Name the cold half.** `mesh_blind_spots` (`scene="{{scene}}"`) lists the meshes that are
61
+ present and essentially never noticed. A hot-spot report that only names hot spots tells you
62
+ nothing about the content you paid to build.
63
+
64
+ 7. **Narrow it with the DSL.** For anything the canned tools do not expose, use the single `query`
65
+ tool: pick the `metric`, bound it with `range`, filter it, and set `format: "summary"` for a
66
+ bounded digest — each summary row carries a `drillQuery` you can send straight back instead of
67
+ rebuilding the filter. Set `compare: { range: <previous window> }` to see whether a hot spot is
68
+ new, and `explain: true` when a result looks wrong or empty: the plan names the capture channel,
69
+ the sample size and the row cap behind it.
70
+
71
+ ## What to report
72
+
73
+ - The two or three real hot-spots, named with the project's own region and mesh names, each with
74
+ its share of attention.
75
+ - The cold areas and the meshes nobody notices.
76
+ - Where gaze fails to convert into interaction, and what that implies for layout and
77
+ call-to-action placement.
78
+
79
+ Carry the caveats: heatmaps are gated on the view/pointer capture channels and are sampled
80
+ (ADR 0012), so a share is a share _of the sampled events_; say so, and say when a result was
81
+ truncated (`meta.truncated`) or sits below the metric's own minimum sample. Do not turn a voxel
82
+ cluster into a claim about one object unless `click_rays` or `flow_links` attributes it to that
83
+ mesh.
84
+
85
+ End with 2–3 concrete layout or content recommendations. If an `annotate` tool is available, leave
86
+ a note on the region you want revisited — a region-scoped annotation is what makes the next report
87
+ open where this one ended. If a `pin_panel` tool is available, pin the heatmap panel you reasoned
88
+ from.