@luanpdd/kit-mcp 1.17.0 → 1.18.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,6 +1,6 @@
1
1
  {
2
- "version": "1.17.0",
3
- "timestamp": "2026-05-09T15:58:57.716Z",
2
+ "version": "1.18.0",
3
+ "timestamp": "2026-05-09T17:01:35.745Z",
4
4
  "files": {
5
5
  "COMANDOS.md": "d24ec61a6ec35db314cc5f2ae287bfb927b794789c8f1d558c55862f5e6534b2",
6
6
  "COMPATIBILITY.md": "794e336a87045cdf0161785b9a7a0975a49abbd80bdd816b8852251fcc8126ca",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luanpdd/kit-mcp",
3
- "version": "1.17.0",
3
+ "version": "1.18.0",
4
4
  "description": "Generic infrastructure to ship YOUR personal kit of agents/commands/skills as an MCP server, with cross-IDE sync (Claude Code, Cursor, Codex, Gemini, Windsurf, Antigravity, Copilot, Trae).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,143 @@
1
+ // OBS-18-01 / OBS-18-02 — in-memory golden signals for kit-mcp server.
2
+ //
3
+ // Phase 94: Eat Your Own Dog Food. The skill `four-golden-signals` says any
4
+ // user-facing service worth its salt instruments Latency + Traffic + Errors
5
+ // + Saturation. The MCP server qualifies — every tool call is a request from
6
+ // an LLM client and tail latency / error rate are exactly the signals an
7
+ // operator wants when something feels off.
8
+ //
9
+ // Scope decisions (see .planning/phases/94-golden-signals-mcp-server/94-CONTEXT.md):
10
+ // - Zero dependencies. Map + array stdlib only — preserves the 6-deps budget
11
+ // that Phase 92.01 fought to maintain and that Phase 93.01 enforces in CI.
12
+ // - In-memory only. No file persistence, no socket export, no OTel SDK.
13
+ // kit-mcp is a developer tool launched on demand by an IDE; cross-process
14
+ // telemetry pipelines are explicit non-goals (see <deferred> block in
15
+ // 94-CONTEXT.md). A future phase can layer OTel on top of this API.
16
+ // - Bounded memory. Histograms cap at HISTOGRAM_CAP=1000 samples per tool
17
+ // with FIFO drop. At cap, p50/p95/p99 over the latest 1000 samples is
18
+ // more useful than an unbounded array that could grow for the lifetime
19
+ // of a long-lived MCP session.
20
+ // - Snapshot is read-only. Returns a fresh plain-object copy so callers
21
+ // can JSON.stringify it without exposing internal Map references.
22
+ //
23
+ // API surface (4 exports):
24
+ // incrementInvocation(tool, status) — counter++ keyed `${tool}:${status}`
25
+ // recordLatency(tool, ms) — push to histogram, FIFO at cap
26
+ // snapshot() — { counters, latency } plain object
27
+ // reset() — clear both maps; called on boot if
28
+ // KIT_MCP_METRICS_RESET=1
29
+ //
30
+ // Boot-time reset honors the env var by calling reset() at module load when
31
+ // the flag is set. This keeps the signal "fresh" for a probe in tests or for
32
+ // an operator who spawned the server with the flag for a clean comparison.
33
+
34
+ const HISTOGRAM_CAP = 1000;
35
+
36
+ const counters = new Map(); // key: `${tool}:${status}` → count (number)
37
+ const histograms = new Map(); // key: tool → number[] (length ≤ HISTOGRAM_CAP)
38
+
39
+ /**
40
+ * Increment the invocation counter for a tool/status pair.
41
+ *
42
+ * @param {string} tool Tool name as it appears in the MCP request payload.
43
+ * @param {'ok'|'error'} [status='ok'] Outcome of the dispatch.
44
+ * @returns {void}
45
+ */
46
+ export function incrementInvocation(tool, status = 'ok') {
47
+ if (typeof tool !== 'string' || tool.length === 0) return;
48
+ const key = `${tool}:${status}`;
49
+ counters.set(key, (counters.get(key) ?? 0) + 1);
50
+ }
51
+
52
+ /**
53
+ * Record an observed latency for a tool. Drops the oldest sample (FIFO) once
54
+ * the per-tool histogram reaches HISTOGRAM_CAP, keeping memory bounded across
55
+ * long-lived MCP sessions.
56
+ *
57
+ * @param {string} tool Tool name.
58
+ * @param {number} ms Elapsed wall-clock time in milliseconds.
59
+ * @returns {void}
60
+ */
61
+ export function recordLatency(tool, ms) {
62
+ if (typeof tool !== 'string' || tool.length === 0) return;
63
+ if (typeof ms !== 'number' || !Number.isFinite(ms) || ms < 0) return;
64
+ let arr = histograms.get(tool);
65
+ if (!arr) {
66
+ arr = [];
67
+ histograms.set(tool, arr);
68
+ }
69
+ arr.push(ms);
70
+ if (arr.length > HISTOGRAM_CAP) arr.shift(); // FIFO drop oldest sample
71
+ }
72
+
73
+ /**
74
+ * Compute a percentile over a sorted ascending array. Linear-interpolation
75
+ * variant matches the typical Prometheus / Datadog reading. For N≤1000
76
+ * (HISTOGRAM_CAP) the sort cost on snapshot is acceptable — snapshots are
77
+ * read on-demand by the metrics-snapshot tool, not on every dispatch.
78
+ *
79
+ * @param {number[]} sorted Ascending-sorted samples.
80
+ * @param {number} p Percentile in [0, 1].
81
+ * @returns {number}
82
+ */
83
+ function percentile(sorted, p) {
84
+ if (sorted.length === 0) return 0;
85
+ if (sorted.length === 1) return sorted[0];
86
+ const rank = p * (sorted.length - 1);
87
+ const lo = Math.floor(rank);
88
+ const hi = Math.ceil(rank);
89
+ if (lo === hi) return sorted[lo];
90
+ const frac = rank - lo;
91
+ return sorted[lo] + (sorted[hi] - sorted[lo]) * frac;
92
+ }
93
+
94
+ /**
95
+ * Build a read-only snapshot of all metrics. Counters are returned as a plain
96
+ * object keyed `${tool}:${status}` → count. Latency is keyed by tool to a
97
+ * `{ p50, p95, p99, count }` triple so a single tool never appears split
98
+ * across status outcomes (latency observation point is a single line in the
99
+ * dispatcher, success and failure both record).
100
+ *
101
+ * @returns {{
102
+ * counters: Record<string, number>,
103
+ * latency: Record<string, { p50: number, p95: number, p99: number, count: number }>
104
+ * }}
105
+ */
106
+ export function snapshot() {
107
+ const out = { counters: {}, latency: {} };
108
+ for (const [key, val] of counters) out.counters[key] = val;
109
+ for (const [tool, samples] of histograms) {
110
+ if (samples.length === 0) continue;
111
+ const sorted = [...samples].sort((a, b) => a - b);
112
+ out.latency[tool] = {
113
+ p50: percentile(sorted, 0.50),
114
+ p95: percentile(sorted, 0.95),
115
+ p99: percentile(sorted, 0.99),
116
+ count: samples.length,
117
+ };
118
+ }
119
+ return out;
120
+ }
121
+
122
+ /**
123
+ * Clear both counters and histograms. Used by tests and by the boot-time
124
+ * KIT_MCP_METRICS_RESET=1 path so an operator can probe a fresh window.
125
+ *
126
+ * @returns {void}
127
+ */
128
+ export function reset() {
129
+ counters.clear();
130
+ histograms.clear();
131
+ }
132
+
133
+ // Boot-time reset honors KIT_MCP_METRICS_RESET=1. We call reset() instead of
134
+ // merely skipping init because the maps are already empty at module load —
135
+ // the call is a no-op today but documents the contract for any future module
136
+ // that imports metrics.js after another module has already populated state.
137
+ if (process.env.KIT_MCP_METRICS_RESET === '1') {
138
+ reset();
139
+ }
140
+
141
+ // Exported for tests only — keeps the API surface explicit while letting unit
142
+ // tests assert on the FIFO behavior at the boundary.
143
+ export const __TEST_HISTOGRAM_CAP = HISTOGRAM_CAP;
@@ -1,10 +1,11 @@
1
- // kit-mcp server — exposes 5 tools, each with action-based dispatch.
1
+ // kit-mcp server — exposes 7 tools, each with action-based dispatch (or none).
2
2
  //
3
- // kit action: list-agents | list-commands | list-skills | get | search
4
- // sync action: targets | status | install | remove
5
- // gates action: list | get | for-stage
6
- // forensics action: collect | summarize | write-learnings | list-replays | record-replay | load-replay
7
- // install action: targets | install | dry-run (registers this MCP into an IDE)
3
+ // kit action: list-agents | list-commands | list-skills | get | search
4
+ // sync action: targets | status | install | remove
5
+ // gates action: list | get | for-stage
6
+ // forensics action: collect | summarize | write-learnings | list-replays | record-replay | load-replay
7
+ // install action: targets | install | dry-run (registers this MCP into an IDE)
8
+ // metrics-snapshot (parameterless) (OBS-18 four-golden-signals readout)
8
9
  //
9
10
  // Transport: stdio (MCP standard).
10
11
 
@@ -30,6 +31,7 @@ import { recordReplay, listReplays, loadReplay, annotateReplay } from '../core/r
30
31
  import { installMcp, listInstallTargets } from './install.js';
31
32
  import { ensureSidecar } from '../ui/auto-spawn.js';
32
33
  import { wrapProgressForUi } from '../ui/wrapper.js';
34
+ import { incrementInvocation, recordLatency, snapshot as metricsSnapshot } from '../core/metrics.js';
33
35
 
34
36
  const TOOLS = [
35
37
  {
@@ -130,6 +132,17 @@ const TOOLS = [
130
132
  required: ['action'],
131
133
  },
132
134
  },
135
+ {
136
+ // OBS-18 (Phase 94.01): expose four-golden-signals data for the MCP server itself.
137
+ // Read-only (no auth needed beyond the underlying transport): returns counters
138
+ // keyed `${tool}:${status}` and per-tool latency p50/p95/p99/count.
139
+ name: 'metrics-snapshot',
140
+ description: 'Read in-memory golden-signals metrics for this MCP server (counters + latency p50/p95/p99 per tool).',
141
+ inputSchema: {
142
+ type: 'object',
143
+ properties: {},
144
+ },
145
+ },
133
146
  ];
134
147
 
135
148
  // DRIFT-13-03: read version from package.json at module load (NOT inside
@@ -292,13 +305,21 @@ async function handleInstall(args) {
292
305
  }
293
306
  }
294
307
 
308
+ // OBS-18 (Phase 94.01): metrics-snapshot is parameterless and read-only.
309
+ // Returns the live snapshot synchronously — no auth, no projectRoot guard
310
+ // (no disk reads, no shell). Wraps in an async fn for handler-API uniformity.
311
+ async function handleMetricsSnapshot() {
312
+ return metricsSnapshot();
313
+ }
314
+
295
315
  const HANDLERS = {
296
- kit: handleKit,
297
- sync: handleSync,
298
- 'reverse-sync':handleReverseSync,
299
- gates: handleGates,
300
- forensics: handleForensics,
301
- install: handleInstall,
316
+ kit: handleKit,
317
+ sync: handleSync,
318
+ 'reverse-sync': handleReverseSync,
319
+ gates: handleGates,
320
+ forensics: handleForensics,
321
+ install: handleInstall,
322
+ 'metrics-snapshot': handleMetricsSnapshot,
302
323
  };
303
324
 
304
325
  function slim(x) {
@@ -330,12 +351,30 @@ export async function createServer() {
330
351
  const { name, arguments: args } = req.params;
331
352
  const handler = HANDLERS[name];
332
353
  if (!handler) {
354
+ // OBS-18 (Phase 94.01): unknown-tool path counts as an error against
355
+ // the unknown name itself — useful signal if a client is mis-spelling
356
+ // a tool name in production. No latency observation (handler never ran).
357
+ incrementInvocation(name || 'unknown', 'error');
333
358
  return { content: [{ type: 'text', text: JSON.stringify({ error: `Unknown tool: ${name}` }) }], isError: true };
334
359
  }
360
+ // OBS-18 (Phase 94.01): timestamp the dispatch boundary. The four-golden-signals
361
+ // skill cares about the *user-facing* latency, which for the MCP server is the
362
+ // time from request receipt (we are inside the SDK callback) to the JSON envelope
363
+ // being ready. Date.now() is sub-millisecond-cheap and aligns with the bucket
364
+ // granularity we report (50/100/250/500ms thresholds in CONTEXT.md).
365
+ const start = Date.now();
335
366
  try {
336
367
  const result = await handler(args ?? {});
368
+ recordLatency(name, Date.now() - start);
369
+ incrementInvocation(name, 'ok');
337
370
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
338
371
  } catch (e) {
372
+ // OBS-18: still record latency on the error path — half the value of a
373
+ // latency histogram is catching tail-latency-then-fail patterns. Status
374
+ // 'error' covers any thrown exception, including Phase 79.01 gates guard
375
+ // and the validateProjectRoot rejection (Phase 83.01).
376
+ recordLatency(name, Date.now() - start);
377
+ incrementInvocation(name, 'error');
339
378
  // SEC-14-06: full stack stays in stderr for operator debug; client envelope is sanitized.
340
379
  // sanitizeMcpError redacts secrets/paths from e.message, preserves e.code (Phase 83
341
380
  // EMANIFESTMISMATCH invariant), and emits NO stack field.