@memlab/mcp-server 2.18.1 → 2.52.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.
Files changed (186) hide show
  1. package/README.md +8 -14
  2. package/bin/.eslintrc.js +26 -0
  3. package/bin/memlab-cli.js +313 -0
  4. package/dist/artifact-classes.d.ts +86 -0
  5. package/dist/artifact-classes.d.ts.map +1 -0
  6. package/dist/artifact-classes.js +261 -0
  7. package/dist/artifact-classes.js.map +1 -0
  8. package/dist/capture-mode.d.ts +60 -0
  9. package/dist/capture-mode.d.ts.map +1 -0
  10. package/dist/capture-mode.js +39 -0
  11. package/dist/capture-mode.js.map +1 -0
  12. package/dist/guardrail.d.ts.map +1 -1
  13. package/dist/guardrail.js +13 -0
  14. package/dist/guardrail.js.map +1 -1
  15. package/dist/heap-state.d.ts +89 -2
  16. package/dist/heap-state.d.ts.map +1 -1
  17. package/dist/heap-state.js +186 -2
  18. package/dist/heap-state.js.map +1 -1
  19. package/dist/index.js +39 -1
  20. package/dist/index.js.map +1 -1
  21. package/dist/progress.d.ts +25 -0
  22. package/dist/progress.d.ts.map +1 -0
  23. package/dist/progress.js +46 -0
  24. package/dist/progress.js.map +1 -0
  25. package/dist/result-handles.d.ts +11 -0
  26. package/dist/result-handles.d.ts.map +1 -0
  27. package/dist/result-handles.js +77 -0
  28. package/dist/result-handles.js.map +1 -0
  29. package/dist/string-index.d.ts +55 -0
  30. package/dist/string-index.d.ts.map +1 -0
  31. package/dist/string-index.js +91 -0
  32. package/dist/string-index.js.map +1 -0
  33. package/dist/tool-registry.d.ts +42 -0
  34. package/dist/tool-registry.d.ts.map +1 -0
  35. package/dist/tool-registry.js +20 -0
  36. package/dist/tool-registry.js.map +1 -0
  37. package/dist/tools/analyze-run.d.ts +12 -0
  38. package/dist/tools/analyze-run.d.ts.map +1 -0
  39. package/dist/tools/analyze-run.js +343 -0
  40. package/dist/tools/analyze-run.js.map +1 -0
  41. package/dist/tools/app-heap.d.ts +12 -0
  42. package/dist/tools/app-heap.d.ts.map +1 -0
  43. package/dist/tools/app-heap.js +140 -0
  44. package/dist/tools/app-heap.js.map +1 -0
  45. package/dist/tools/batch.d.ts +12 -0
  46. package/dist/tools/batch.d.ts.map +1 -0
  47. package/dist/tools/batch.js +168 -0
  48. package/dist/tools/batch.js.map +1 -0
  49. package/dist/tools/cache-analysis.d.ts.map +1 -1
  50. package/dist/tools/cache-analysis.js +156 -5
  51. package/dist/tools/cache-analysis.js.map +1 -1
  52. package/dist/tools/class-histogram.d.ts.map +1 -1
  53. package/dist/tools/class-histogram.js +14 -3
  54. package/dist/tools/class-histogram.js.map +1 -1
  55. package/dist/tools/collection-trend.d.ts +55 -0
  56. package/dist/tools/collection-trend.d.ts.map +1 -0
  57. package/dist/tools/collection-trend.js +315 -0
  58. package/dist/tools/collection-trend.js.map +1 -0
  59. package/dist/tools/detached-dom.d.ts +2 -0
  60. package/dist/tools/detached-dom.d.ts.map +1 -1
  61. package/dist/tools/detached-dom.js +291 -20
  62. package/dist/tools/detached-dom.js.map +1 -1
  63. package/dist/tools/dev-artifacts.d.ts +62 -6
  64. package/dist/tools/dev-artifacts.d.ts.map +1 -1
  65. package/dist/tools/dev-artifacts.js +466 -23
  66. package/dist/tools/dev-artifacts.js.map +1 -1
  67. package/dist/tools/dominator-attribution.d.ts +12 -0
  68. package/dist/tools/dominator-attribution.d.ts.map +1 -0
  69. package/dist/tools/dominator-attribution.js +189 -0
  70. package/dist/tools/dominator-attribution.js.map +1 -0
  71. package/dist/tools/dominator-chain.d.ts +12 -0
  72. package/dist/tools/dominator-chain.d.ts.map +1 -0
  73. package/dist/tools/dominator-chain.js +92 -0
  74. package/dist/tools/dominator-chain.js.map +1 -0
  75. package/dist/tools/duplicate-objects.d.ts +12 -0
  76. package/dist/tools/duplicate-objects.d.ts.map +1 -0
  77. package/dist/tools/duplicate-objects.js +172 -0
  78. package/dist/tools/duplicate-objects.js.map +1 -0
  79. package/dist/tools/duplicated-strings.d.ts.map +1 -1
  80. package/dist/tools/duplicated-strings.js +46 -38
  81. package/dist/tools/duplicated-strings.js.map +1 -1
  82. package/dist/tools/eval.d.ts +1 -0
  83. package/dist/tools/eval.d.ts.map +1 -1
  84. package/dist/tools/eval.js +785 -28
  85. package/dist/tools/eval.js.map +1 -1
  86. package/dist/tools/explain-delta.d.ts +12 -0
  87. package/dist/tools/explain-delta.d.ts.map +1 -0
  88. package/dist/tools/explain-delta.js +170 -0
  89. package/dist/tools/explain-delta.js.map +1 -0
  90. package/dist/tools/find-nodes-by-class.d.ts.map +1 -1
  91. package/dist/tools/find-nodes-by-class.js +112 -13
  92. package/dist/tools/find-nodes-by-class.js.map +1 -1
  93. package/dist/tools/finding-index.d.ts +22 -0
  94. package/dist/tools/finding-index.d.ts.map +1 -0
  95. package/dist/tools/finding-index.js +247 -0
  96. package/dist/tools/finding-index.js.map +1 -0
  97. package/dist/tools/for-each.d.ts.map +1 -1
  98. package/dist/tools/for-each.js +15 -3
  99. package/dist/tools/for-each.js.map +1 -1
  100. package/dist/tools/get-referrers.d.ts.map +1 -1
  101. package/dist/tools/get-referrers.js +17 -0
  102. package/dist/tools/get-referrers.js.map +1 -1
  103. package/dist/tools/hunt-report.d.ts +12 -0
  104. package/dist/tools/hunt-report.d.ts.map +1 -0
  105. package/dist/tools/hunt-report.js +186 -0
  106. package/dist/tools/hunt-report.js.map +1 -0
  107. package/dist/tools/hypothesis.d.ts +12 -0
  108. package/dist/tools/hypothesis.d.ts.map +1 -0
  109. package/dist/tools/hypothesis.js +221 -0
  110. package/dist/tools/hypothesis.js.map +1 -0
  111. package/dist/tools/intern-opportunities.d.ts.map +1 -1
  112. package/dist/tools/intern-opportunities.js +112 -47
  113. package/dist/tools/intern-opportunities.js.map +1 -1
  114. package/dist/tools/ladder.d.ts +33 -0
  115. package/dist/tools/ladder.d.ts.map +1 -0
  116. package/dist/tools/ladder.js +266 -0
  117. package/dist/tools/ladder.js.map +1 -0
  118. package/dist/tools/largest-objects.d.ts.map +1 -1
  119. package/dist/tools/largest-objects.js +6 -2
  120. package/dist/tools/largest-objects.js.map +1 -1
  121. package/dist/tools/leak-report.d.ts +12 -0
  122. package/dist/tools/leak-report.d.ts.map +1 -0
  123. package/dist/tools/leak-report.js +256 -0
  124. package/dist/tools/leak-report.js.map +1 -0
  125. package/dist/tools/load-snapshot.d.ts +18 -0
  126. package/dist/tools/load-snapshot.d.ts.map +1 -1
  127. package/dist/tools/load-snapshot.js +211 -13
  128. package/dist/tools/load-snapshot.js.map +1 -1
  129. package/dist/tools/map-entries.d.ts +12 -0
  130. package/dist/tools/map-entries.d.ts.map +1 -0
  131. package/dist/tools/map-entries.js +139 -0
  132. package/dist/tools/map-entries.js.map +1 -0
  133. package/dist/tools/match-object.d.ts +12 -0
  134. package/dist/tools/match-object.d.ts.map +1 -0
  135. package/dist/tools/match-object.js +165 -0
  136. package/dist/tools/match-object.js.map +1 -0
  137. package/dist/tools/object-shape.d.ts.map +1 -1
  138. package/dist/tools/object-shape.js +53 -5
  139. package/dist/tools/object-shape.js.map +1 -1
  140. package/dist/tools/population-diff.d.ts +57 -0
  141. package/dist/tools/population-diff.d.ts.map +1 -0
  142. package/dist/tools/population-diff.js +338 -0
  143. package/dist/tools/population-diff.js.map +1 -0
  144. package/dist/tools/retainer-layers.d.ts +23 -0
  145. package/dist/tools/retainer-layers.d.ts.map +1 -0
  146. package/dist/tools/retainer-layers.js +179 -0
  147. package/dist/tools/retainer-layers.js.map +1 -0
  148. package/dist/tools/retainer-summary.d.ts +1 -0
  149. package/dist/tools/retainer-summary.d.ts.map +1 -1
  150. package/dist/tools/retainer-summary.js +128 -32
  151. package/dist/tools/retainer-summary.js.map +1 -1
  152. package/dist/tools/retainer-trace.d.ts.map +1 -1
  153. package/dist/tools/retainer-trace.js +4 -2
  154. package/dist/tools/retainer-trace.js.map +1 -1
  155. package/dist/tools/search-nodes.d.ts.map +1 -1
  156. package/dist/tools/search-nodes.js +5 -1
  157. package/dist/tools/search-nodes.js.map +1 -1
  158. package/dist/tools/search-strings.d.ts.map +1 -1
  159. package/dist/tools/search-strings.js +53 -7
  160. package/dist/tools/search-strings.js.map +1 -1
  161. package/dist/tools/sequence-analysis.d.ts +49 -0
  162. package/dist/tools/sequence-analysis.d.ts.map +1 -1
  163. package/dist/tools/sequence-analysis.js +266 -86
  164. package/dist/tools/sequence-analysis.js.map +1 -1
  165. package/dist/tools/shape-histogram.d.ts.map +1 -1
  166. package/dist/tools/shape-histogram.js +9 -2
  167. package/dist/tools/shape-histogram.js.map +1 -1
  168. package/dist/tools/string-patterns.d.ts.map +1 -1
  169. package/dist/tools/string-patterns.js +5 -4
  170. package/dist/tools/string-patterns.js.map +1 -1
  171. package/dist/tools/tools-index.d.ts +12 -0
  172. package/dist/tools/tools-index.d.ts.map +1 -0
  173. package/dist/tools/tools-index.js +342 -0
  174. package/dist/tools/tools-index.js.map +1 -0
  175. package/dist/tools/verify-fix.d.ts +23 -0
  176. package/dist/tools/verify-fix.d.ts.map +1 -0
  177. package/dist/tools/verify-fix.js +211 -0
  178. package/dist/tools/verify-fix.js.map +1 -0
  179. package/dist/tools/weakmap-entries.d.ts.map +1 -1
  180. package/dist/tools/weakmap-entries.js +6 -1
  181. package/dist/tools/weakmap-entries.js.map +1 -1
  182. package/dist/utils.d.ts +146 -1
  183. package/dist/utils.d.ts.map +1 -1
  184. package/dist/utils.js +335 -10
  185. package/dist/utils.js.map +1 -1
  186. package/package.json +5 -4
package/README.md CHANGED
@@ -76,21 +76,12 @@ Then configure (`~/.claude.json` for Claude Code, or `.mcp.json` for Cursor/Wind
76
76
 
77
77
  ## Auto-Approving Tool Permissions
78
78
 
79
- By default, Claude Code prompts you to approve each MCP tool call. To auto-approve all MemLab tools, add this to your `~/.claude/settings.json` (user-level) or `.claude/settings.json` (project-level):
79
+ By default, Claude Code prompts you to approve each MCP tool call. Every MemLab tool except `memlab_eval` and `memlab_for_each` is strictly read-only heap analysis and safe to auto-approve.
80
80
 
81
- ```json
82
- {
83
- "permissions": {
84
- "allow": [
85
- "mcp__memlab__*"
86
- ]
87
- }
88
- }
89
- ```
81
+ > [!CAUTION]
82
+ > **Do not auto-approve `memlab_eval` or `memlab_for_each`.** They execute arbitrary JavaScript with the full privileges of the MCP server process (see the warning under [`memlab_eval`](#memlab_eval)). Leave them on manual approval so each invocation is reviewed. Avoid the `mcp__memlab__*` wildcard, which would auto-approve them too.
90
83
 
91
- The naming convention is `mcp__<server-name>__*` where `memlab` matches the key you used in your MCP server config. The `*` wildcard auto-approves all tools from that server.
92
-
93
- You can also allowlist individual tools for granular control, add this to your `~/.claude/settings.json` (user-level) or `.claude/settings.json` (project-level):
84
+ To auto-approve the read-only tools while keeping the two code-execution tools on manual approval, allowlist the read-only tools explicitly in your `~/.claude/settings.json` (user-level) or `.claude/settings.json` (project-level):
94
85
 
95
86
  ```json
96
87
  {
@@ -371,7 +362,10 @@ Input: { report: "list"|"full_analysis"|"detached_dom"|"duplicated_strings"|...
371
362
 
372
363
  ### `memlab_eval`
373
364
 
374
- Execute arbitrary JavaScript against the loaded heap snapshot in a sandboxed VM. Has access to `snapshot`, `utils`, and `helpers` but no filesystem/network access.
365
+ Execute arbitrary JavaScript against the loaded heap snapshot. Has access to `snapshot`, `utils`, and `helpers`.
366
+
367
+ > [!WARNING]
368
+ > **This tool runs arbitrary code with the full privileges of the MCP server process.** It uses `node:vm` to scope the globals it injects, but [`node:vm` is not a security sandbox](https://nodejs.org/api/vm.html) — code can reach the host realm (e.g. `Object.constructor('return process')()`) and from there touch the filesystem, network, and shell. Treat calling `memlab_eval` as equivalent to running code on your machine. The same applies to `memlab_for_each`, whose `filter`/`map`/`reduce` code strings are executed the same way. Do not auto-approve these two tools, and be aware that heap snapshot contents are attacker-influenceable input — a prompt-injection could steer the model into running hostile code here.
375
369
 
376
370
  ```
377
371
  Input: { code: "...", timeout_ms?: 30000 }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @format
8
+ * @oncall memory_lab
9
+ */
10
+
11
+ // Files in this directory are npm `bin` entry points: package.json declares
12
+ // them as executables, so POSIX requires `#!/usr/bin/env node` on line 1.
13
+ //
14
+ // `fb-www/docblock-first` treats anything preceding the file docblock as
15
+ // disqualifying, and the shebang counts — verified by removing it, at which
16
+ // point the rule stops firing. The two requirements are therefore mutually
17
+ // exclusive for this directory, and the rule's stated rationale (ESLint skips
18
+ // pragma parsing, producing spurious no-undef errors for Flow utility types)
19
+ // does not apply: these are plain CommonJS/ESM Node scripts with no Flow types.
20
+ //
21
+ // Scoped to bin/ so the rule keeps applying to every other file in the package.
22
+ module.exports = {
23
+ rules: {
24
+ 'fb-www/docblock-first': 'off',
25
+ },
26
+ };
@@ -0,0 +1,313 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @format
8
+ * @oncall memory_lab
9
+ */
10
+ /// usr/bin/env node
11
+
12
+ /**
13
+ * Non-MCP command line for the memlab MCP server.
14
+ *
15
+ * Why this exists: the server is only ever reachable through an MCP client, so
16
+ * when a host fails to attach it (a dropped stdio handshake, a plugin that never
17
+ * finishes connecting) every tool in the package becomes unavailable at once,
18
+ * with no diagnostic and no fallback. That happened in practice and cost an
19
+ * agent ~20 minutes and a hand-written JSON-RPC client before any heap could be
20
+ * analyzed at all. The server itself was healthy the whole time.
21
+ *
22
+ * This speaks the same stdio JSON-RPC the host would, so the tools stay usable
23
+ * without one:
24
+ *
25
+ * memlab-cli doctor
26
+ * memlab-cli list
27
+ * memlab-cli schema memlab_load_snapshot
28
+ * memlab-cli call memlab_snapshot_header '{"file_path":"/tmp/a.heapsnapshot"}'
29
+ * memlab-cli script steps.jsonl # one {"tool":..,"args":{..}} per line
30
+ *
31
+ * `script` (and multiple `call` pairs) reuse ONE server process, which matters:
32
+ * the server is stateful and a large snapshot costs minutes to load, so a
33
+ * process per call pays that repeatedly.
34
+ */
35
+
36
+ import {spawn} from 'child_process';
37
+ import fs from 'fs';
38
+ import path from 'path';
39
+ import {fileURLToPath} from 'url';
40
+
41
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
42
+
43
+ function resolveServerPath() {
44
+ if (process.env.MEMLAB_MCP_SERVER) {
45
+ return process.env.MEMLAB_MCP_SERVER;
46
+ }
47
+ // Packaged layout is bin/ next to dist/; the plugin copies this script to the
48
+ // install root, where dist/ is a direct child.
49
+ const candidates = [
50
+ path.join(HERE, '..', 'dist', 'index.js'),
51
+ path.join(HERE, 'dist', 'index.js'),
52
+ ];
53
+ for (const c of candidates) {
54
+ if (fs.existsSync(c)) {
55
+ return c;
56
+ }
57
+ }
58
+ return candidates[0];
59
+ }
60
+
61
+ /**
62
+ * The interpreter to run the SERVER with, which is not necessarily the one
63
+ * running this CLI: `node` on PATH is frequently older than the package's
64
+ * `>= 18` engine requirement, and spawning the server with it fails at module
65
+ * load with an opaque syntax error. Mirrors the plugin start.sh lookup.
66
+ */
67
+ function resolveNodeBin() {
68
+ if (Number(process.versions.node.split('.')[0]) >= 18) {
69
+ return process.execPath;
70
+ }
71
+ for (const c of ['/usr/local/bin/claude_code/node']) {
72
+ if (fs.existsSync(c)) {
73
+ return c;
74
+ }
75
+ }
76
+ return process.execPath;
77
+ }
78
+
79
+ class Client {
80
+ constructor(serverPath) {
81
+ this.proc = spawn(
82
+ resolveNodeBin(),
83
+ ['--max-old-space-size=8192', serverPath],
84
+ {stdio: ['pipe', 'pipe', 'pipe']},
85
+ );
86
+ this.nextId = 0;
87
+ this.buf = '';
88
+ this.pending = new Map();
89
+ this.stderr = [];
90
+ // Drain stderr. A chatty server that fills this pipe would otherwise block
91
+ // its own writes and deadlock the session.
92
+ this.proc.stderr.on('data', d => {
93
+ this.stderr.push(String(d));
94
+ if (this.stderr.length > 400) {
95
+ this.stderr.splice(0, 200);
96
+ }
97
+ });
98
+ this.proc.stdout.on('data', d => this.onData(String(d)));
99
+ this.proc.on('exit', code => {
100
+ for (const {reject} of this.pending.values()) {
101
+ reject(
102
+ new Error(
103
+ `memlab server exited (code ${code}). stderr tail:\n${this.stderr.slice(-40).join('')}`,
104
+ ),
105
+ );
106
+ }
107
+ this.pending.clear();
108
+ });
109
+ }
110
+
111
+ onData(chunk) {
112
+ this.buf += chunk;
113
+ let nl;
114
+ while ((nl = this.buf.indexOf('\n')) >= 0) {
115
+ const line = this.buf.slice(0, nl).trim();
116
+ this.buf = this.buf.slice(nl + 1);
117
+ if (!line) {
118
+ continue;
119
+ }
120
+ let msg;
121
+ try {
122
+ msg = JSON.parse(line);
123
+ } catch {
124
+ continue;
125
+ }
126
+ const entry = this.pending.get(msg.id);
127
+ if (!entry) {
128
+ continue;
129
+ }
130
+ this.pending.delete(msg.id);
131
+ if (msg.error) {
132
+ entry.reject(new Error(JSON.stringify(msg.error)));
133
+ } else {
134
+ entry.resolve(msg.result);
135
+ }
136
+ }
137
+ }
138
+
139
+ request(method, params) {
140
+ const id = ++this.nextId;
141
+ this.proc.stdin.write(
142
+ JSON.stringify({jsonrpc: '2.0', id, method, params}) + '\n',
143
+ );
144
+ return new Promise((resolve, reject) =>
145
+ this.pending.set(id, {resolve, reject}),
146
+ );
147
+ }
148
+
149
+ notify(method, params) {
150
+ this.proc.stdin.write(
151
+ JSON.stringify({jsonrpc: '2.0', method, params}) + '\n',
152
+ );
153
+ }
154
+
155
+ async init() {
156
+ await this.request('initialize', {
157
+ protocolVersion: '2024-11-05',
158
+ capabilities: {},
159
+ clientInfo: {name: 'memlab-cli', version: '1.0'},
160
+ });
161
+ this.notify('notifications/initialized', {});
162
+ }
163
+
164
+ async listTools() {
165
+ return (await this.request('tools/list', {})).tools ?? [];
166
+ }
167
+
168
+ async callTool(name, args) {
169
+ const res = await this.request('tools/call', {name, arguments: args});
170
+ return (res.content ?? [])
171
+ .map(c => (c.type === 'text' ? c.text : JSON.stringify(c)))
172
+ .join('\n');
173
+ }
174
+
175
+ close() {
176
+ this.proc.stdin.end();
177
+ }
178
+ }
179
+
180
+ async function doctor(serverPath) {
181
+ const major = Number(process.versions.node.split('.')[0]);
182
+ const nodeBin = resolveNodeBin();
183
+ const lines = [
184
+ `cli node: ${process.versions.node}${major >= 18 ? '' : ' (below the >= 18 engine requirement)'}`,
185
+ `server node: ${nodeBin}${nodeBin === process.execPath && major < 18 ? ' — NO >= 18 interpreter found; the server will fail to load' : ''}`,
186
+ ];
187
+ lines.push(`server path: ${serverPath}`);
188
+ const exists = fs.existsSync(serverPath);
189
+ lines.push(`server present: ${exists ? 'yes' : 'NO'}`);
190
+ if (!exists) {
191
+ lines.push(
192
+ 'Fix: build the package (`npm run build-pkg`), or set MEMLAB_MCP_SERVER to a built dist/index.js.',
193
+ );
194
+ console.log(lines.join('\n'));
195
+ return 1;
196
+ }
197
+ // The decisive check: can the server actually start and answer? A corrupted
198
+ // or partial node_modules only shows up here, not from a file listing.
199
+ const client = new Client(serverPath);
200
+ const timer = setTimeout(() => client.proc.kill('SIGKILL'), 60000);
201
+ try {
202
+ await client.init();
203
+ const tools = await client.listTools();
204
+ lines.push(`handshake: OK`);
205
+ lines.push(`tools registered: ${tools.length}`);
206
+ console.log(lines.join('\n'));
207
+ return 0;
208
+ } catch (err) {
209
+ lines.push(`handshake: FAILED — ${err.message}`);
210
+ lines.push(
211
+ 'Fix: check the install dir for build-errors.log / deps-check-errors.log, ' +
212
+ 'then rebuild (the plugin start.sh does a clean rebuild when dependencies fail to load).',
213
+ );
214
+ console.log(lines.join('\n'));
215
+ return 1;
216
+ } finally {
217
+ clearTimeout(timer);
218
+ client.close();
219
+ }
220
+ }
221
+
222
+ async function main() {
223
+ const [cmd, ...rest] = process.argv.slice(2);
224
+ const serverPath = resolveServerPath();
225
+
226
+ if (!cmd || cmd === '--help' || cmd === '-h') {
227
+ console.log(
228
+ [
229
+ 'Usage: memlab-cli <command>',
230
+ '',
231
+ ' doctor check node, server presence, and a live handshake',
232
+ ' list list tool names and one-line descriptions',
233
+ ' schema <tool> print a tool input schema as JSON',
234
+ ' call <tool> <json> [...] call one or more tools in ONE server session',
235
+ ' script <file.jsonl> run {"tool":..,"args":{..}} lines in ONE session',
236
+ '',
237
+ 'Server is resolved from $MEMLAB_MCP_SERVER, else dist/index.js next to this script.',
238
+ ].join('\n'),
239
+ );
240
+ return 0;
241
+ }
242
+
243
+ if (cmd === 'doctor') {
244
+ return doctor(serverPath);
245
+ }
246
+
247
+ const client = new Client(serverPath);
248
+ try {
249
+ await client.init();
250
+ if (cmd === 'list') {
251
+ for (const t of await client.listTools()) {
252
+ console.log(`${t.name}\t${(t.description ?? '').slice(0, 150)}`);
253
+ }
254
+ } else if (cmd === 'schema') {
255
+ const want = rest[0];
256
+ const tool = (await client.listTools()).find(t => t.name === want);
257
+ if (!tool) {
258
+ console.error(`Unknown tool: ${want}`);
259
+ return 1;
260
+ }
261
+ console.log(JSON.stringify(tool.inputSchema ?? {}, null, 2));
262
+ } else if (cmd === 'call') {
263
+ for (let i = 0; i < rest.length; i += 2) {
264
+ const tool = rest[i];
265
+ const args = rest[i + 1] ? JSON.parse(rest[i + 1]) : {};
266
+ console.log(`\n===== ${tool} =====`);
267
+ // Sequential by construction: JSON-RPC over ONE stdio socket to a
268
+ // stateful server (a loaded snapshot). These cannot be parallelised —
269
+ // the server answers one request at a time and later calls depend on
270
+ // earlier ones.
271
+ // eslint-disable-next-line no-await-in-loop
272
+ console.log(await client.callTool(tool, args));
273
+ }
274
+ } else if (cmd === 'script') {
275
+ const file = rest[0];
276
+ // Sequential by construction: these are JSON-RPC calls over ONE stdio
277
+ // socket to a stateful server, so they cannot be parallelised — the next
278
+ // request depends on the previous one having been answered.
279
+
280
+ for (const raw of fs.readFileSync(file, 'utf8').split('\n')) {
281
+ const line = raw.trim();
282
+ if (!line || line.startsWith('#')) {
283
+ continue;
284
+ }
285
+ const spec = JSON.parse(line);
286
+ console.log(`\n===== ${spec.tool} =====`);
287
+ // Sequential by construction: JSON-RPC over ONE stdio socket to a
288
+ // stateful server (a loaded snapshot). These cannot be parallelised —
289
+ // the server answers one request at a time and later calls depend on
290
+ // earlier ones.
291
+ // eslint-disable-next-line no-await-in-loop
292
+ console.log(await client.callTool(spec.tool, spec.args ?? {}));
293
+ }
294
+ } else {
295
+ console.error(`Unknown command: ${cmd}. Try --help.`);
296
+ return 1;
297
+ }
298
+ return 0;
299
+ } finally {
300
+ client.close();
301
+ }
302
+ }
303
+
304
+ // Terminal by construction: both arms call process.exit, so there is nothing
305
+ // left to chain and no rejection can escape.
306
+ // eslint-disable-next-line fb-www/promise-termination
307
+ main().then(
308
+ code => process.exit(code ?? 0),
309
+ err => {
310
+ console.error(err.message ?? String(err));
311
+ process.exit(1);
312
+ },
313
+ );
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @format
8
+ * @oncall memory_lab
9
+ */
10
+ /**
11
+ * Registry of heap classes whose growth is a MEASUREMENT artifact rather than an
12
+ * application leak.
13
+ *
14
+ * Motivation: across a six-round leak hunt, every single class reported at the
15
+ * top of `sequence_analysis` as "↑ every step (LEAK signal)" belonged to one of
16
+ * the families below — CDP inspector bookkeeping, V8 JIT warmup, or captured
17
+ * error stacks. None was ever a leak. An agent that does not already know this
18
+ * list reads the label and reports a false leak, which is exactly what happened
19
+ * repeatedly before the families were catalogued.
20
+ *
21
+ * These are deliberately matched on CLASS NAME only. Name matching cannot prove
22
+ * that a given instance is an artifact, so the classification is used to
23
+ * annotate and down-rank rows — never to hide them outright (callers can always
24
+ * pass `include_artifacts: true`). Anything that is genuinely ambiguous is left
25
+ * unclassified on purpose: a false "this is an artifact" is far more damaging
26
+ * than a false "this might be a leak".
27
+ */
28
+ export type ArtifactKind = 'warmup' | 'ax' | 'cdp-network' | 'cdp-perf' | 'cdp-console' | 'stack-capture';
29
+ export declare function artifactLabel(kind: ArtifactKind): string;
30
+ export declare function artifactNote(kind: ArtifactKind): string;
31
+ /**
32
+ * Where a non-production string came from. Both kinds are equally unshippable,
33
+ * but they call for different follow-up: `harness` means re-run without the
34
+ * bridge attached to measure the app cleanly, `dev-build` means measure a
35
+ * production build.
36
+ */
37
+ export type NonProductionStringKind = 'harness' | 'dev-build';
38
+ /**
39
+ * Classify a string VALUE as non-production (harness-injected or DEV-build
40
+ * only), or null when it is app content that really does ship.
41
+ */
42
+ export declare function classifyNonProductionString(value: string): {
43
+ kind: NonProductionStringKind;
44
+ what: string;
45
+ } | null;
46
+ /**
47
+ * Back-compatible wrapper returning just the description. Prefer
48
+ * {@link classifyNonProductionString} when the caller can act on the kind.
49
+ */
50
+ export declare function classifyHarnessString(value: string): string | null;
51
+ /**
52
+ * React Fast Refresh module-scope registries, by the edge name that binds each
53
+ * one. These live in the refresh runtime's closure and are reachable only
54
+ * through compiled-code constant pools, so a retainer-root scan does not find
55
+ * them and they classify as `production` unless matched by name here. See
56
+ * `tools/dev-artifacts` for the measured cost of missing them.
57
+ */
58
+ export declare const REACT_REFRESH_REGISTRY_EDGE_NAMES: ReadonlySet<string>;
59
+ /**
60
+ * The rarest property of the react-refresh signature record. Callers gate on
61
+ * this before confirming the full shape, so a scan stays one name compare per
62
+ * edge on everything else.
63
+ */
64
+ export declare const REACT_REFRESH_SIGNATURE_GATE_PROP = "getCustomHooks";
65
+ /**
66
+ * Recognize the per-type signature record react-refresh stores via
67
+ * `setSignature(type, key, forceReset, getCustomHooks)` — the value side of
68
+ * `allSignaturesByType`, shaped `{forceReset, ownKey, fullKey, getCustomHooks}`.
69
+ */
70
+ export declare function isReactRefreshSignatureShape(propNames: Iterable<string>): boolean;
71
+ /**
72
+ * Recognize the parent object SHAPE of an MCP/devtools tool manifest entry.
73
+ *
74
+ * The bridge's tool definitions carry long `.description` strings — one per tool
75
+ * — and a per-load intern pool would happily "save" them. The shape is
76
+ * distinctive enough to match directly, and matching the shape (rather than the
77
+ * description text) keeps app strings that merely have a `description` property
78
+ * out of it.
79
+ */
80
+ export declare function isHarnessToolManifestShape(propNames: ReadonlyArray<string>): boolean;
81
+ /**
82
+ * Classify a class NAME as a known measurement artifact, or null when it is a
83
+ * genuine leak candidate.
84
+ */
85
+ export declare function classifyArtifact(name: string): ArtifactKind | null;
86
+ //# sourceMappingURL=artifact-classes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"artifact-classes.d.ts","sourceRoot":"","sources":["../src/artifact-classes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,MAAM,YAAY,GACpB,QAAQ,GACR,IAAI,GACJ,aAAa,GACb,UAAU,GACV,aAAa,GACb,eAAe,CAAC;AAwGpB,wBAAgB,aAAa,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,CAExD;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,CAEvD;AAED;;;;;GAKG;AACH,MAAM,MAAM,uBAAuB,GAAG,SAAS,GAAG,WAAW,CAAC;AAkE9D;;;GAGG;AACH,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,MAAM,GACZ;IAAC,IAAI,EAAE,uBAAuB,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAC,GAAG,IAAI,CAKtD;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAElE;AAED;;;;;;GAMG;AACH,eAAO,MAAM,iCAAiC,EAAE,WAAW,CAAC,MAAM,CAWhE,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,iCAAiC,mBAAmB,CAAC;AASlE;;;;GAIG;AACH,wBAAgB,4BAA4B,CAC1C,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,GAC1B,OAAO,CAOT;AAED;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,aAAa,CAAC,MAAM,CAAC,GAC/B,OAAO,CAQT;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CASlE"}