@uptimizr/mcp 1.1.1 → 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.
package/dist/server.d.ts CHANGED
@@ -1,18 +1,75 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { type CollectorClient } from "@uptimizr/agent-core";
3
3
  /**
4
- * Build the Uptimizr MCP server: a read-only `McpServer` whose tools each wrap
5
- * one collector query endpoint via the injected `CollectorClient`. The server
6
- * holds no business logic — it forwards validated arguments and returns the
7
- * collector's JSON (ADR 0005 / ADR 0017). Alongside the tools it exposes
4
+ * How to build the server for one session.
5
+ *
6
+ * Deliberately an **options bag**: the collector-hosted transport (#313) builds
7
+ * one server per authenticated MCP session, and both the `query:raw` tools
8
+ * (#314) and the metadata write tools (#310) key off the same field, so this is
9
+ * the one place a per-key decision is threaded through.
10
+ */
11
+ export interface CreateMcpServerOptions {
12
+ /**
13
+ * The capability set of the API key this server instance is bound to —
14
+ * `query`, `query:raw`, `annotate`, … (ADR 0051 §7).
15
+ *
16
+ * Supplied by the **collector-hosted** Streamable HTTP transport (`/mcp`),
17
+ * which resolves the key on every request and builds one server per session,
18
+ * so the surface a session sees can match what its key may actually do. The
19
+ * stdio entry point omits it: that process never learns the key's
20
+ * capabilities, and the collector is the enforcement point either way, so an
21
+ * omitted set means "register the read-only catalog and let the collector
22
+ * refuse anything the key may not do".
23
+ *
24
+ * Tools outside the set are simply not registered: the raw-session tools of
25
+ * #314 only for a key holding `query:raw`, the metadata write tools of #310
26
+ * only for one holding `annotate`. Unknown values are ignored, and omitting
27
+ * the option serves the ordinary `query` surface — exactly what every caller
28
+ * written before #310/#313/#314 gets. Deliberately typed as plain strings so
29
+ * this package keeps its dependency-free footprint — it must not reach into
30
+ * `@uptimizr/db` for the capability union (see `__tests__/dependencies.test.ts`).
31
+ */
32
+ capabilities?: readonly string[];
33
+ }
34
+ /**
35
+ * Ask the collector what the configured key may do (`GET /api/v1/whoami`).
36
+ *
37
+ * Never throws: an older collector without the route, an unreachable one, or a
38
+ * key that cannot even read all yield an empty capability set, and the server
39
+ * then starts read-only rather than not starting at all. A degraded but useful
40
+ * server beats no server.
41
+ */
42
+ export declare function fetchKeyCapabilities(client: CollectorClient): Promise<readonly string[]>;
43
+ /**
44
+ * Build the Uptimizr MCP server: an `McpServer` whose tools each wrap one
45
+ * collector endpoint via the injected `CollectorClient`. The server holds no
46
+ * business logic — it forwards validated arguments and returns the collector's
47
+ * JSON (ADR 0005 / ADR 0017). Alongside the tools it exposes
8
48
  * capability-discovery **resources** and curated analysis **prompts** so agents
9
49
  * can self-orient (ADR 0050 §7).
10
50
  *
11
- * The tool catalog is generated from the `@uptimizr/db` metric registry
51
+ * The **analytics** tool catalog is generated from the metric registry
12
52
  * (ADR 0051 §1), so `tools/list` covers every metric the collector serves on an
13
53
  * endpoint. Each tool advertises the registry-derived `outputSchema` and returns
14
- * both `structuredContent` (the typed `{ rows }` envelope) and the `content`
15
- * text a client without structured-output support still reads.
54
+ * both `structuredContent` — the `format` envelope the call asked for, which
55
+ * since #336 is `{ meta, rows }` by default — and the `content` text a client
56
+ * without structured-output support still reads. That catalog is entirely
57
+ * read-only: **events cannot be written, altered or deleted through this
58
+ * server** (ADR 0051 §9).
59
+ *
60
+ * `options.capabilities` is what the **key** behind `client` is allowed to do
61
+ * (`GET /api/v1/whoami`). Tools whose endpoint needs more than the ordinary
62
+ * `query` capability are registered only when the key really holds it, so
63
+ * `tools/list` describes what this session can actually do rather than what the
64
+ * collector could do for somebody else: the raw-session tools of #314 behind
65
+ * `query:raw`, the **metadata** tools of #310 — annotations, glossary, saved
66
+ * analyses — behind `annotate`. Omit it and only the `query` surface is served,
67
+ * the safe default for a caller that has not looked the key up.
68
+ *
69
+ * The factory is **transport-agnostic**: `bin.ts` connects it to stdio, and the
70
+ * collector connects one instance per authenticated Streamable HTTP session at
71
+ * `/mcp` (ADR 0051 §7), passing that session's key capabilities through
72
+ * `options`. Both get the same tools, resources and prompts from this one place.
16
73
  */
17
- export declare function createMcpServer(client: CollectorClient): McpServer;
74
+ export declare function createMcpServer(client: CollectorClient, options?: CreateMcpServerOptions): McpServer;
18
75
  //# sourceMappingURL=server.d.ts.map
@@ -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,62 +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
+
35
95
  ## Result formats (`format`)
36
96
 
37
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`.
38
100
 
39
101
  - `summary` — **prefer this.** A bounded digest: `ranked` top rows, a `series` trend, merged spatial
40
102
  `clusters`, or a single `record`, with shares, a sample size, the metric's `caveats` and a
41
103
  templated `reading` sentence. Capped at the metric's `limits.maxSummaryRows`, so a 500-bin heatmap
42
104
  costs the same as a 5-bin one. `reading` and `caveats` are templated by pure code — no model —
43
105
  so identical rows always produce identical words.
44
- - `table` — `{ meta, rows }`: every row plus the metric, range, applied filters, sample size, row
45
- count, a truncation flag and the registry limits. Use it when you need the rows *and* the context
46
- to judge them.
47
- - `full` — the bare rows, unchanged. Today's catalog default; pass `format` explicitly.
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.
48
110
 
49
111
  Shares appear only where the measure can honestly be summed (an FPS or ratio metric reports
50
112
  `total: null` and no shares). Cluster coordinates are grid indices — multiply by the effective
51
- `cellSize` for world space. `session_meta` and `scene_representation` are stored records, not
52
- aggregations, and take no `format`.
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`.
53
120
 
54
121
  ## Resources and prompts
55
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.
56
129
  - `uptimizr://capabilities` (`application/json`) — static descriptor: schema version, canonical
57
130
  event types, the tool catalog, the parameter-semantics glossary, and `metrics` (the whole registry
58
131
  with grain, column units, row JSON Schema, filters, limits, interpretation, caveats, source
59
- channels). No collector call. Read it first.
132
+ channels). No collector call. Read it after the context.
60
133
  - `uptimizr://scenes` (`application/json`) — the live scene ids with recent activity, i.e. the valid
61
134
  values for the `scene` parameter. Fetched through the read-only query API.
62
- - Prompts: `weekly_scene_health` (optional `scene`), `attention_hotspots` (required `scene`),
63
- `xr_comfort_review` (optional `scene`) — each renders one user message sequencing the read-only
64
- tools; they fetch nothing themselves.
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.
65
173
 
66
174
  ## Required key capability
67
175
 
68
- `UPTIMIZR_API_KEY` needs the `query` capability, and only that
176
+ `UPTIMIZR_API_KEY` needs the `query` capability
69
177
  (`uptimizr new-key <projectId> --capabilities query --label "mcp-agent"`; `query` is the default).
70
- `query:raw` is **deliberately not required** — no tool here reads raw per-session, replay or
71
- live-follow data. `annotate` and `ingest` are never used either. `GET /api/v1/whoami` reports what a
72
- key holds; a key missing a required capability is refused with `403`.
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`.
73
183
 
74
184
  ## Key exports
75
185
 
76
- - `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).
77
191
  - `createCollectorClient(config)` — the thin GET-only collector client.
78
192
  - `readMcpConfig()` — read `UPTIMIZR_COLLECTOR_URL` + `UPTIMIZR_API_KEY` from the environment.
79
193
  - `readTools` — the read-only tool catalog (one entry per query endpoint), generated from the
80
- `@uptimizr/metrics` metric registry: 69 tools, each with an input schema, an MCP `outputSchema` and the
81
- 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.