@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.
- package/kit/file-manifest.json +2 -2
- package/package.json +1 -1
- package/src/core/metrics.js +143 -0
- package/src/mcp-server/index.js +51 -12
package/kit/file-manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.
|
|
3
|
-
"timestamp": "2026-05-
|
|
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.
|
|
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;
|
package/src/mcp-server/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
// kit-mcp server — exposes
|
|
1
|
+
// kit-mcp server — exposes 7 tools, each with action-based dispatch (or none).
|
|
2
2
|
//
|
|
3
|
-
// kit
|
|
4
|
-
// sync
|
|
5
|
-
// gates
|
|
6
|
-
// forensics
|
|
7
|
-
// install
|
|
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:
|
|
297
|
-
sync:
|
|
298
|
-
'reverse-sync':handleReverseSync,
|
|
299
|
-
gates:
|
|
300
|
-
forensics:
|
|
301
|
-
install:
|
|
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.
|