@rog0x/mcp-perf-tools 1.0.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/src/index.ts ADDED
@@ -0,0 +1,257 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import { z } from "zod";
6
+
7
+ import {
8
+ runBenchmark,
9
+ compareBenchmarks,
10
+ formatBenchmarkResult,
11
+ } from "./tools/benchmark.js";
12
+ import {
13
+ analyzeMemory,
14
+ takeMemorySnapshot,
15
+ clearSnapshotHistory,
16
+ formatSnapshot,
17
+ } from "./tools/memory-analyzer.js";
18
+ import { estimateBigO } from "./tools/big-o-estimator.js";
19
+ import { analyzeBundle } from "./tools/bundle-analyzer.js";
20
+ import { runLoadTest } from "./tools/load-tester.js";
21
+
22
+ const server = new McpServer({
23
+ name: "mcp-perf-tools",
24
+ version: "1.0.0",
25
+ });
26
+
27
+ // -------------------------------------------------------------------
28
+ // Tool 1: benchmark
29
+ // -------------------------------------------------------------------
30
+ server.tool(
31
+ "benchmark",
32
+ "Benchmark JavaScript code execution. Run a snippet N times and measure min/max/avg/p95/p99 time and ops/sec. Optionally compare two implementations.",
33
+ {
34
+ code: z.string().describe("JavaScript code to benchmark"),
35
+ iterations: z
36
+ .number()
37
+ .int()
38
+ .min(1)
39
+ .default(1000)
40
+ .describe("Number of iterations to run"),
41
+ compareCode: z
42
+ .string()
43
+ .optional()
44
+ .describe("Optional second implementation to compare against"),
45
+ labelA: z
46
+ .string()
47
+ .default("A")
48
+ .describe("Label for the first implementation"),
49
+ labelB: z
50
+ .string()
51
+ .default("B")
52
+ .describe("Label for the second implementation"),
53
+ },
54
+ async ({ code, iterations, compareCode, labelA, labelB }) => {
55
+ try {
56
+ if (compareCode) {
57
+ const result = compareBenchmarks(
58
+ code,
59
+ labelA,
60
+ compareCode,
61
+ labelB,
62
+ iterations
63
+ );
64
+ return { content: [{ type: "text", text: result.summary }] };
65
+ }
66
+
67
+ const result = runBenchmark(code, iterations);
68
+ return {
69
+ content: [{ type: "text", text: formatBenchmarkResult(result) }],
70
+ };
71
+ } catch (err: unknown) {
72
+ const msg = err instanceof Error ? err.message : String(err);
73
+ return {
74
+ content: [{ type: "text", text: `Benchmark error: ${msg}` }],
75
+ isError: true,
76
+ };
77
+ }
78
+ }
79
+ );
80
+
81
+ // -------------------------------------------------------------------
82
+ // Tool 2: memory_analyze
83
+ // -------------------------------------------------------------------
84
+ server.tool(
85
+ "memory_analyze",
86
+ "Analyze Node.js memory usage: heap used/total, RSS, external, array buffers. Tracks snapshots over time and detects potential memory leaks via growth trend analysis.",
87
+ {
88
+ action: z
89
+ .enum(["snapshot", "analyze", "clear"])
90
+ .default("analyze")
91
+ .describe(
92
+ "snapshot: take a single reading; analyze: full analysis with trend detection; clear: reset history"
93
+ ),
94
+ },
95
+ async ({ action }) => {
96
+ try {
97
+ if (action === "clear") {
98
+ clearSnapshotHistory();
99
+ return {
100
+ content: [{ type: "text", text: "Memory snapshot history cleared." }],
101
+ };
102
+ }
103
+ if (action === "snapshot") {
104
+ const snap = takeMemorySnapshot();
105
+ return {
106
+ content: [{ type: "text", text: formatSnapshot(snap) }],
107
+ };
108
+ }
109
+ // analyze
110
+ const analysis = analyzeMemory();
111
+ return {
112
+ content: [{ type: "text", text: analysis.summary }],
113
+ };
114
+ } catch (err: unknown) {
115
+ const msg = err instanceof Error ? err.message : String(err);
116
+ return {
117
+ content: [{ type: "text", text: `Memory analysis error: ${msg}` }],
118
+ isError: true,
119
+ };
120
+ }
121
+ }
122
+ );
123
+
124
+ // -------------------------------------------------------------------
125
+ // Tool 3: big_o_estimate
126
+ // -------------------------------------------------------------------
127
+ server.tool(
128
+ "big_o_estimate",
129
+ "Estimate Big O complexity from empirical data. Provide input sizes and corresponding execution times to get the best-fit complexity class (O(1), O(log n), O(n), O(n log n), O(n^2), O(n^3), O(2^n)) with confidence scores and growth curve.",
130
+ {
131
+ inputSizes: z
132
+ .array(z.number().positive())
133
+ .min(3)
134
+ .describe("Array of input sizes (e.g., [100, 500, 1000, 5000])"),
135
+ executionTimesMs: z
136
+ .array(z.number().nonnegative())
137
+ .min(3)
138
+ .describe(
139
+ "Array of execution times in milliseconds, corresponding to each input size"
140
+ ),
141
+ },
142
+ async ({ inputSizes, executionTimesMs }) => {
143
+ try {
144
+ const result = estimateBigO(inputSizes, executionTimesMs);
145
+ return {
146
+ content: [{ type: "text", text: result.summary }],
147
+ };
148
+ } catch (err: unknown) {
149
+ const msg = err instanceof Error ? err.message : String(err);
150
+ return {
151
+ content: [{ type: "text", text: `Big O estimation error: ${msg}` }],
152
+ isError: true,
153
+ };
154
+ }
155
+ }
156
+ );
157
+
158
+ // -------------------------------------------------------------------
159
+ // Tool 4: bundle_analyze
160
+ // -------------------------------------------------------------------
161
+ server.tool(
162
+ "bundle_analyze",
163
+ "Analyze a JavaScript bundle file: raw size, gzip estimate, detected modules, largest modules, and tree-shaking opportunities.",
164
+ {
165
+ filePath: z
166
+ .string()
167
+ .describe("Absolute path to the JavaScript bundle file to analyze"),
168
+ },
169
+ async ({ filePath }) => {
170
+ try {
171
+ const result = analyzeBundle(filePath);
172
+ return {
173
+ content: [{ type: "text", text: result.summary }],
174
+ };
175
+ } catch (err: unknown) {
176
+ const msg = err instanceof Error ? err.message : String(err);
177
+ return {
178
+ content: [{ type: "text", text: `Bundle analysis error: ${msg}` }],
179
+ isError: true,
180
+ };
181
+ }
182
+ }
183
+ );
184
+
185
+ // -------------------------------------------------------------------
186
+ // Tool 5: load_test
187
+ // -------------------------------------------------------------------
188
+ server.tool(
189
+ "load_test",
190
+ "Run a simple HTTP load test: send N concurrent requests to a URL, measure response times, error rate, throughput (req/sec), and percentile latencies.",
191
+ {
192
+ url: z.string().url().describe("Target URL to test"),
193
+ totalRequests: z
194
+ .number()
195
+ .int()
196
+ .min(1)
197
+ .default(100)
198
+ .describe("Total number of requests to send"),
199
+ concurrency: z
200
+ .number()
201
+ .int()
202
+ .min(1)
203
+ .default(10)
204
+ .describe("Number of concurrent requests"),
205
+ method: z
206
+ .string()
207
+ .default("GET")
208
+ .describe("HTTP method (GET, POST, PUT, DELETE, etc.)"),
209
+ headers: z
210
+ .record(z.string(), z.string())
211
+ .optional()
212
+ .describe("Optional HTTP headers as key-value pairs"),
213
+ body: z.string().optional().describe("Optional request body"),
214
+ timeoutMs: z
215
+ .number()
216
+ .int()
217
+ .min(1000)
218
+ .default(30000)
219
+ .describe("Request timeout in milliseconds"),
220
+ },
221
+ async ({ url, totalRequests, concurrency, method, headers, body, timeoutMs }) => {
222
+ try {
223
+ const result = await runLoadTest({
224
+ url,
225
+ totalRequests,
226
+ concurrency,
227
+ method,
228
+ headers,
229
+ body,
230
+ timeoutMs,
231
+ });
232
+ return {
233
+ content: [{ type: "text", text: result.summary }],
234
+ };
235
+ } catch (err: unknown) {
236
+ const msg = err instanceof Error ? err.message : String(err);
237
+ return {
238
+ content: [{ type: "text", text: `Load test error: ${msg}` }],
239
+ isError: true,
240
+ };
241
+ }
242
+ }
243
+ );
244
+
245
+ // -------------------------------------------------------------------
246
+ // Start server
247
+ // -------------------------------------------------------------------
248
+ async function main(): Promise<void> {
249
+ const transport = new StdioServerTransport();
250
+ await server.connect(transport);
251
+ console.error("mcp-perf-tools server running on stdio");
252
+ }
253
+
254
+ main().catch((err) => {
255
+ console.error("Fatal error:", err);
256
+ process.exit(1);
257
+ });
@@ -0,0 +1,113 @@
1
+ import { performance } from "node:perf_hooks";
2
+
3
+ export interface BenchmarkResult {
4
+ iterations: number;
5
+ minMs: number;
6
+ maxMs: number;
7
+ avgMs: number;
8
+ medianMs: number;
9
+ p95Ms: number;
10
+ p99Ms: number;
11
+ opsPerSecond: number;
12
+ totalMs: number;
13
+ timings: number[];
14
+ }
15
+
16
+ export interface ComparisonResult {
17
+ a: BenchmarkResult & { label: string };
18
+ b: BenchmarkResult & { label: string };
19
+ fasterLabel: string;
20
+ speedupFactor: number;
21
+ summary: string;
22
+ }
23
+
24
+ function percentile(sorted: number[], p: number): number {
25
+ if (sorted.length === 0) return 0;
26
+ const idx = (p / 100) * (sorted.length - 1);
27
+ const lower = Math.floor(idx);
28
+ const upper = Math.ceil(idx);
29
+ if (lower === upper) return sorted[lower];
30
+ return sorted[lower] + (sorted[upper] - sorted[lower]) * (idx - lower);
31
+ }
32
+
33
+ export function runBenchmark(code: string, iterations: number): BenchmarkResult {
34
+ const timings: number[] = [];
35
+
36
+ // Wrap the code in a function so it can be executed repeatedly
37
+ const fn = new Function(code);
38
+
39
+ // Warmup: 10% of iterations or at least 3
40
+ const warmupCount = Math.max(3, Math.floor(iterations * 0.1));
41
+ for (let i = 0; i < warmupCount; i++) {
42
+ fn();
43
+ }
44
+
45
+ for (let i = 0; i < iterations; i++) {
46
+ const start = performance.now();
47
+ fn();
48
+ const end = performance.now();
49
+ timings.push(end - start);
50
+ }
51
+
52
+ const sorted = [...timings].sort((a, b) => a - b);
53
+ const totalMs = timings.reduce((s, t) => s + t, 0);
54
+ const avgMs = totalMs / timings.length;
55
+
56
+ return {
57
+ iterations,
58
+ minMs: sorted[0],
59
+ maxMs: sorted[sorted.length - 1],
60
+ avgMs,
61
+ medianMs: percentile(sorted, 50),
62
+ p95Ms: percentile(sorted, 95),
63
+ p99Ms: percentile(sorted, 99),
64
+ opsPerSecond: totalMs > 0 ? (iterations / totalMs) * 1000 : Infinity,
65
+ totalMs,
66
+ timings: sorted,
67
+ };
68
+ }
69
+
70
+ export function compareBenchmarks(
71
+ codeA: string,
72
+ labelA: string,
73
+ codeB: string,
74
+ labelB: string,
75
+ iterations: number
76
+ ): ComparisonResult {
77
+ const resultA = runBenchmark(codeA, iterations);
78
+ const resultB = runBenchmark(codeB, iterations);
79
+
80
+ const fasterIsA = resultA.avgMs < resultB.avgMs;
81
+ const fasterLabel = fasterIsA ? labelA : labelB;
82
+ const speedupFactor = fasterIsA
83
+ ? resultB.avgMs / resultA.avgMs
84
+ : resultA.avgMs / resultB.avgMs;
85
+
86
+ const summary = [
87
+ `"${fasterLabel}" is ${speedupFactor.toFixed(2)}x faster.`,
88
+ ` ${labelA}: avg ${resultA.avgMs.toFixed(4)}ms, p95 ${resultA.p95Ms.toFixed(4)}ms, ${resultA.opsPerSecond.toFixed(0)} ops/s`,
89
+ ` ${labelB}: avg ${resultB.avgMs.toFixed(4)}ms, p95 ${resultB.p95Ms.toFixed(4)}ms, ${resultB.opsPerSecond.toFixed(0)} ops/s`,
90
+ ].join("\n");
91
+
92
+ return {
93
+ a: { ...resultA, label: labelA },
94
+ b: { ...resultB, label: labelB },
95
+ fasterLabel,
96
+ speedupFactor,
97
+ summary,
98
+ };
99
+ }
100
+
101
+ export function formatBenchmarkResult(result: BenchmarkResult): string {
102
+ return [
103
+ `Benchmark Results (${result.iterations} iterations):`,
104
+ ` Min: ${result.minMs.toFixed(4)} ms`,
105
+ ` Max: ${result.maxMs.toFixed(4)} ms`,
106
+ ` Avg: ${result.avgMs.toFixed(4)} ms`,
107
+ ` Median: ${result.medianMs.toFixed(4)} ms`,
108
+ ` P95: ${result.p95Ms.toFixed(4)} ms`,
109
+ ` P99: ${result.p99Ms.toFixed(4)} ms`,
110
+ ` Ops/s: ${result.opsPerSecond.toFixed(0)}`,
111
+ ` Total: ${result.totalMs.toFixed(2)} ms`,
112
+ ].join("\n");
113
+ }
@@ -0,0 +1,175 @@
1
+ export type ComplexityClass =
2
+ | "O(1)"
3
+ | "O(log n)"
4
+ | "O(n)"
5
+ | "O(n log n)"
6
+ | "O(n^2)"
7
+ | "O(n^3)"
8
+ | "O(2^n)"
9
+ | "unknown";
10
+
11
+ interface FitResult {
12
+ complexity: ComplexityClass;
13
+ r2: number;
14
+ description: string;
15
+ }
16
+
17
+ function linearRegression(
18
+ xs: number[],
19
+ ys: number[]
20
+ ): { slope: number; intercept: number; r2: number } {
21
+ const n = xs.length;
22
+ if (n < 2) return { slope: 0, intercept: ys[0] ?? 0, r2: 0 };
23
+
24
+ const sumX = xs.reduce((a, b) => a + b, 0);
25
+ const sumY = ys.reduce((a, b) => a + b, 0);
26
+ const sumXY = xs.reduce((a, x, i) => a + x * ys[i], 0);
27
+ const sumX2 = xs.reduce((a, x) => a + x * x, 0);
28
+
29
+ const denom = n * sumX2 - sumX * sumX;
30
+ if (denom === 0) return { slope: 0, intercept: sumY / n, r2: 0 };
31
+
32
+ const slope = (n * sumXY - sumX * sumY) / denom;
33
+ const intercept = (sumY - slope * sumX) / n;
34
+
35
+ const meanY = sumY / n;
36
+ const ssTot = ys.reduce((a, y) => a + (y - meanY) ** 2, 0);
37
+ const ssRes = ys.reduce(
38
+ (a, y, i) => a + (y - (slope * xs[i] + intercept)) ** 2,
39
+ 0
40
+ );
41
+ const r2 = ssTot === 0 ? 1 : 1 - ssRes / ssTot;
42
+
43
+ return { slope, intercept, r2 };
44
+ }
45
+
46
+ function tryFit(
47
+ sizes: number[],
48
+ times: number[],
49
+ transform: (n: number) => number,
50
+ label: ComplexityClass,
51
+ description: string
52
+ ): FitResult {
53
+ const transformed = sizes.map(transform);
54
+ // Check for non-finite values
55
+ if (transformed.some((v) => !isFinite(v))) {
56
+ return { complexity: label, r2: -Infinity, description };
57
+ }
58
+ const { r2 } = linearRegression(transformed, times);
59
+ return { complexity: label, r2, description };
60
+ }
61
+
62
+ export interface BigOEstimate {
63
+ bestFit: ComplexityClass;
64
+ confidence: number;
65
+ allFits: Array<{ complexity: ComplexityClass; r2: number }>;
66
+ growthCurve: string;
67
+ summary: string;
68
+ }
69
+
70
+ export function estimateBigO(
71
+ inputSizes: number[],
72
+ executionTimesMs: number[]
73
+ ): BigOEstimate {
74
+ if (inputSizes.length !== executionTimesMs.length) {
75
+ throw new Error("inputSizes and executionTimesMs must have the same length");
76
+ }
77
+ if (inputSizes.length < 3) {
78
+ throw new Error("Need at least 3 data points for a meaningful estimate");
79
+ }
80
+
81
+ const fits: FitResult[] = [
82
+ tryFit(
83
+ inputSizes,
84
+ executionTimesMs,
85
+ (_n) => 1,
86
+ "O(1)",
87
+ "Constant time: performance does not change with input size."
88
+ ),
89
+ tryFit(
90
+ inputSizes,
91
+ executionTimesMs,
92
+ (n) => Math.log2(n),
93
+ "O(log n)",
94
+ "Logarithmic: performance grows slowly, typical of binary search or balanced trees."
95
+ ),
96
+ tryFit(
97
+ inputSizes,
98
+ executionTimesMs,
99
+ (n) => n,
100
+ "O(n)",
101
+ "Linear: performance scales proportionally with input size."
102
+ ),
103
+ tryFit(
104
+ inputSizes,
105
+ executionTimesMs,
106
+ (n) => n * Math.log2(n),
107
+ "O(n log n)",
108
+ "Linearithmic: typical of efficient sorting algorithms (mergesort, heapsort)."
109
+ ),
110
+ tryFit(
111
+ inputSizes,
112
+ executionTimesMs,
113
+ (n) => n * n,
114
+ "O(n^2)",
115
+ "Quadratic: nested iteration over input, common in naive sorting or pairwise comparisons."
116
+ ),
117
+ tryFit(
118
+ inputSizes,
119
+ executionTimesMs,
120
+ (n) => n * n * n,
121
+ "O(n^3)",
122
+ "Cubic: triple-nested loops, e.g., naive matrix multiplication."
123
+ ),
124
+ tryFit(
125
+ inputSizes,
126
+ executionTimesMs,
127
+ (n) => Math.pow(2, n),
128
+ "O(2^n)",
129
+ "Exponential: brute-force recursive algorithms, rapidly becomes infeasible."
130
+ ),
131
+ ];
132
+
133
+ // Sort by R^2 descending (best fit first)
134
+ fits.sort((a, b) => b.r2 - a.r2);
135
+
136
+ const best = fits[0];
137
+ const confidence = Math.max(0, Math.min(1, best.r2));
138
+
139
+ // Build a simple ASCII growth curve description
140
+ const growthCurve = buildGrowthCurve(inputSizes, executionTimesMs);
141
+
142
+ const allFits = fits.map((f) => ({ complexity: f.complexity, r2: f.r2 }));
143
+
144
+ const summary = [
145
+ `Big O Estimate: ${best.complexity} (confidence: ${(confidence * 100).toFixed(1)}%)`,
146
+ `${best.description}`,
147
+ "",
148
+ "All fits (by R² descending):",
149
+ ...allFits.map(
150
+ (f) => ` ${f.complexity.padEnd(10)} R² = ${f.r2.toFixed(4)}`
151
+ ),
152
+ "",
153
+ "Growth Curve:",
154
+ growthCurve,
155
+ ].join("\n");
156
+
157
+ return { bestFit: best.complexity, confidence, allFits, growthCurve, summary };
158
+ }
159
+
160
+ function buildGrowthCurve(sizes: number[], times: number[]): string {
161
+ const maxTime = Math.max(...times);
162
+ const maxBarWidth = 40;
163
+ const lines: string[] = [];
164
+
165
+ for (let i = 0; i < sizes.length; i++) {
166
+ const barLen =
167
+ maxTime > 0 ? Math.round((times[i] / maxTime) * maxBarWidth) : 0;
168
+ const bar = "\u2588".repeat(barLen);
169
+ const sizeStr = String(sizes[i]).padStart(10);
170
+ const timeStr = times[i].toFixed(2).padStart(10);
171
+ lines.push(` n=${sizeStr} ${timeStr}ms ${bar}`);
172
+ }
173
+
174
+ return lines.join("\n");
175
+ }