@amadeus-it-group/ngrx-devtool 0.2.2 → 1.1.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # NgRx DevTool
2
2
 
3
- A development tool for visualizing and debugging NgRx state management in Angular applications. Real-time action monitoring, effect tracking, state visualization, diff viewer, and performance metrics no browser extensions needed.
3
+ A development tool for visualizing and debugging NgRx state management in Angular applications. Real-time action monitoring, effect tracking, state visualization, diff viewer, and performance metrics. No browser extensions needed.
4
4
 
5
5
  ## Install
6
6
 
package/index.js CHANGED
@@ -5,11 +5,13 @@ var ws_1 = require("ws");
5
5
  var path = require("path");
6
6
  var express = require("express");
7
7
  var http_1 = require("http");
8
+ var mcp_server_1 = require("./mcp-server");
8
9
  var chalk = require('chalk');
9
10
  var PORT_WS = 4000;
10
11
  var PORT_UI = '3000';
11
12
  var wss = new ws_1.WebSocketServer({ port: PORT_WS });
12
13
  var clients = [];
14
+ var store = (0, mcp_server_1.createStore)();
13
15
  var logo = "\n ***@ +**\n ** ******%\n @** .......-:: ++%\n #****% ................... *+\n ** . ..................... +\n #*.... .............:--=- :. =\n # . ...............:-%*%% %. =\n ** ..................*. *-+@. =\n %*** .......... ** ...... ....... +\n *# ....:::#********# .... .. .... =:\n * .. ...:::****+=@@@@@@@@@@@ ....... #=\n %* ..:. ...:: +*+.@@@@@@@@@@@@@@@@@@@@%###%%###*+\n # ... ...:::++=@@@@@@@@%%%%%%%%@@@@@@@@@#%%-## ** =\n *# . ..:::*++@@@@@@%%%%%%##%%%%%%%%@@@@@@@+###+:** ..\n ** ..:::=+-@@@@@%%% ##%%%%@@@@@@*.#@%+# ##..\n %* ..:::*=%@@@@%%%%% ##%%%%%%@@@@+%%%%%*# #. *\n *# ..:::.=@@@@@%%%%%%########* ####%%%%@@@@%*%## ## .\n * .::::+.@@@@%%%%%%%%%%%#####* @*###%%%%@%@#%%#%-@% #.\n * . :::::=@@%%%%%%%%%%#####%@= *####%%%%%%*### .%##%.\n * .. .::::=@@%%%%%%#### *####%%%%%%%## %-# .\n :* ..:::-@@%%%%####* @###*# *####%%%%%%#* ## -:\n ..*#* .:::-%@%%%%#### ######* *#####%%*##@ @# .:=*\n =+ + =..:::%@%%%%#### ######* *#####%%#* .. ..:@+\n %+ -= +..-: %%%%%####. ####%%%% =.. ... @#*\n *+ = . + .--%%%%%%####* % ##%%%#-.... ... # *-\n @*:++ : += -:%%%#######******#########%%%%%%%%%%%# =. #*:*\n ** ** : ++ =:%%%%##*********####%%%%%%%%%%%%%%@ :::...** :**\n ** ** . . .== . ### ++++++***####%%%%%%%%%%+ --:::: #* ::@#-\n . ** ## === %** .::-++*###%% :.+-:::::::::@*% =---##%\n . ** *#@ =--== :=**#%%%%% :..:....::.====-%**:.\n +#@## *-: %*###%%%###- :::==++==@%#%..\n +**:*## . .......:::::+===++==%%##.:.\n . **#=#### ....:::--------==.-===+%%%#%::.\n . . **#++#### .....:.::::::::-:::-====%%%*#%-:-:\n ... *##-+#####@ .. .::::::. .:::---%%%%#*#%---::\n ..:. **###*######%%@ .. ... @#%%%%%%#*#%.--=::\n .... **##%=*####%%%%%%%%%%%#%****## ---:::\n ..... .*****%#%#=+***--####%=-:-:-:::\n ::::====---- ===--=::::\n . .:.:::-:::--:::::::\n .........:\n";
14
16
  console.log(chalk.magenta(logo));
15
17
  console.log(chalk.dim('─'.repeat(60)));
@@ -18,6 +20,7 @@ console.log(chalk.dim('─'.repeat(60)));
18
20
  wss.on('connection', function (socket) {
19
21
  clients.push(socket);
20
22
  socket.on('message', function (message) {
23
+ (0, mcp_server_1.captureMessage)(store, message.toString());
21
24
  clients.forEach(function (client) {
22
25
  if (client !== socket && client.readyState === ws_1.WebSocket.OPEN) {
23
26
  client.send(message);
@@ -26,6 +29,7 @@ wss.on('connection', function (socket) {
26
29
  });
27
30
  });
28
31
  var app = express();
32
+ (0, mcp_server_1.registerMcpEndpoint)(app, express, store);
29
33
  app.use(express.static(path.resolve(__dirname, 'ngrx-devtool-ui/browser')));
30
34
  app.get('*', function (_req, res) {
31
35
  res.sendFile(path.resolve(__dirname, 'ngrx-devtool-ui/browser/index.html'));
@@ -45,6 +49,11 @@ process.on('SIGINT', function () {
45
49
  console.log('');
46
50
  console.log(chalk.green(" \u2713 WebSocket"), chalk.dim("ws://localhost:".concat(PORT_WS)));
47
51
  console.log(chalk.green(" \u2713 UI Server"), chalk.dim("http://localhost:".concat(PORT_UI)));
52
+ console.log(chalk.green(" \u2713 AI / MCP "), chalk.dim("http://localhost:".concat(PORT_UI, "/mcp")));
53
+ console.log('');
54
+ console.log(chalk.dim(' Connect Copilot / Claude / Cursor by adding to .vscode/mcp.json:'));
55
+ console.log(chalk.dim(' { "servers": { "ngrx-devtool": { "type": "http",'));
56
+ console.log(chalk.dim(" \"url\": \"http://localhost:".concat(PORT_UI, "/mcp\" } } }")));
48
57
  console.log('');
49
58
  console.log(chalk.dim('─'.repeat(60)));
50
59
  console.log(chalk.dim(' Press'), chalk.white('Ctrl+C'), chalk.dim('to stop all servers'));
package/mcp-server.js ADDED
@@ -0,0 +1,424 @@
1
+ "use strict";
2
+ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
3
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
4
+ if (ar || !(i in from)) {
5
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
6
+ ar[i] = from[i];
7
+ }
8
+ }
9
+ return to.concat(ar || Array.prototype.slice.call(from));
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.createStore = createStore;
13
+ exports.captureMessage = captureMessage;
14
+ exports.registerMcpEndpoint = registerMcpEndpoint;
15
+ var MCP_PROTOCOL_VERSION = '2024-11-05';
16
+ var MAX_ENTRIES = 500;
17
+ function createStore() {
18
+ return { actions: [], effects: [], timings: [], clearCount: 0 };
19
+ }
20
+ function push(arr, item) {
21
+ arr.push(item);
22
+ if (arr.length > MAX_ENTRIES)
23
+ arr.shift();
24
+ }
25
+ function captureMessage(store, raw) {
26
+ var _a;
27
+ var msg;
28
+ try {
29
+ msg = typeof raw === 'string' ? JSON.parse(raw) : JSON.parse(String(raw));
30
+ }
31
+ catch (_b) {
32
+ return;
33
+ }
34
+ if (!isCapturedMessage(msg))
35
+ return;
36
+ if (typeof msg.appName === 'string')
37
+ store.appName = msg.appName;
38
+ switch (msg.type) {
39
+ case 'STATE_CHANGE':
40
+ // NgRx dispatches @ngrx/store/init exactly once per app bootstrap. Treat
41
+ // it as the start of a fresh session and drop data from previous runs, so
42
+ // the MCP view tracks the current app session instead of accumulating
43
+ // stale actions/effects across reloads (which drifts from the live UI).
44
+ if (((_a = msg.action) === null || _a === void 0 ? void 0 : _a.type) === '@ngrx/store/init') {
45
+ store.actions.length = 0;
46
+ store.effects.length = 0;
47
+ store.timings.length = 0;
48
+ }
49
+ push(store.actions, msg);
50
+ break;
51
+ case 'EFFECT_EVENT':
52
+ push(store.effects, msg);
53
+ break;
54
+ case 'RENDER_TIMING':
55
+ push(store.timings, msg);
56
+ break;
57
+ case 'TIMELINE_CLEARED':
58
+ case 'CLEAR_REQUEST':
59
+ clearStore(store, 'websocket');
60
+ break;
61
+ default:
62
+ break;
63
+ }
64
+ }
65
+ function clearStore(store, source) {
66
+ store.actions.length = 0;
67
+ store.effects.length = 0;
68
+ store.timings.length = 0;
69
+ store.clearCount += 1;
70
+ store.lastClearedAt = new Date().toISOString();
71
+ return { clearedAt: store.lastClearedAt, clearCount: store.clearCount, source: source };
72
+ }
73
+ function diffStates(before, after, base, out) {
74
+ if (base === void 0) { base = ''; }
75
+ if (out === void 0) { out = []; }
76
+ if (out.length >= 100)
77
+ return out;
78
+ if (before === after)
79
+ return out;
80
+ var bothObjects = isIndexable(before) && isIndexable(after) &&
81
+ !Array.isArray(before) === !Array.isArray(after);
82
+ if (!bothObjects) {
83
+ out.push({ path: base || '(root)', before: before, after: after });
84
+ return out;
85
+ }
86
+ var beforeRecord = before;
87
+ var afterRecord = after;
88
+ var keys = Object.keys(beforeRecord);
89
+ for (var _i = 0, _a = Object.keys(afterRecord); _i < _a.length; _i++) {
90
+ var k = _a[_i];
91
+ if (!(k in beforeRecord))
92
+ keys.push(k);
93
+ }
94
+ for (var _b = 0, keys_1 = keys; _b < keys_1.length; _b++) {
95
+ var key = keys_1[_b];
96
+ var path = base ? "".concat(base, ".").concat(key) : key;
97
+ if (!(key in beforeRecord)) {
98
+ out.push({ path: path, before: undefined, after: afterRecord[key] });
99
+ }
100
+ else if (!(key in afterRecord)) {
101
+ out.push({ path: path, before: beforeRecord[key], after: undefined });
102
+ }
103
+ else {
104
+ diffStates(beforeRecord[key], afterRecord[key], path, out);
105
+ }
106
+ if (out.length >= 100)
107
+ break;
108
+ }
109
+ return out;
110
+ }
111
+ function actionType(msg) {
112
+ var _a, _b;
113
+ return (_b = (_a = msg === null || msg === void 0 ? void 0 : msg.action) === null || _a === void 0 ? void 0 : _a.type) !== null && _b !== void 0 ? _b : '[unknown]';
114
+ }
115
+ /**
116
+ * Raw STATE_CHANGE messages don't know whether an action was dispatched by a
117
+ * user or emitted by an effect. Reconstruct that link from the EFFECT_EVENT
118
+ * stream: any action that an effect "emitted" is effect-sourced. Mirrors the
119
+ * correlation the UI does in `messagesWithPrevState`.
120
+ */
121
+ function buildEffectActionMap(store) {
122
+ var _a, _b, _c;
123
+ var map = new Map();
124
+ for (var _i = 0, _d = store.effects; _i < _d.length; _i++) {
125
+ var e = _d[_i];
126
+ if (((_a = e.effectEvent) === null || _a === void 0 ? void 0 : _a.lifecycle) === 'emitted' && e.action) {
127
+ map.set(e.action, (_c = (_b = e.effectName) !== null && _b !== void 0 ? _b : e.effectEvent.name) !== null && _c !== void 0 ? _c : 'effect');
128
+ }
129
+ }
130
+ return map;
131
+ }
132
+ function getActionTimeline(store, limit) {
133
+ var effectMap = buildEffectActionMap(store);
134
+ var items = store.actions.slice(-limit).map(function (m) {
135
+ var _a, _b, _c, _d, _e, _f;
136
+ var type = actionType(m);
137
+ var effectName = (_a = m.effectName) !== null && _a !== void 0 ? _a : effectMap.get(type);
138
+ return {
139
+ type: type,
140
+ source: m.isEffectResult || effectMap.has(type) ? 'effect' : 'user',
141
+ effectName: effectName,
142
+ timestamp: m.timestamp,
143
+ renderTimeMs: (_c = (_b = m.renderTiming) === null || _b === void 0 ? void 0 : _b.totalTime) !== null && _c !== void 0 ? _c : (_d = m.renderPerformance) === null || _d === void 0 ? void 0 : _d.renderTime,
144
+ warnings: (_f = (_e = m.performance) === null || _e === void 0 ? void 0 : _e.warnings) === null || _f === void 0 ? void 0 : _f.map(function (w) { return w.message; }),
145
+ };
146
+ });
147
+ return { totalCaptured: store.actions.length, returned: items.length, actions: items };
148
+ }
149
+ function getEffects(store, limit) {
150
+ var items = store.effects.slice(-limit).map(function (m) {
151
+ var _a, _b, _c, _d, _e, _f;
152
+ return ({
153
+ effect: (_a = m.effectName) !== null && _a !== void 0 ? _a : (_b = m.effectEvent) === null || _b === void 0 ? void 0 : _b.name,
154
+ lifecycle: (_c = m.effectEvent) === null || _c === void 0 ? void 0 : _c.lifecycle,
155
+ resultAction: m.action,
156
+ durationMs: (_d = m.effectEvent) === null || _d === void 0 ? void 0 : _d.duration,
157
+ dispatch: (_e = m.effectEvent) === null || _e === void 0 ? void 0 : _e.dispatch,
158
+ timestamp: m.timestamp,
159
+ errorMessage: (_f = m.effectEvent) === null || _f === void 0 ? void 0 : _f.errorMessage,
160
+ });
161
+ });
162
+ return { totalCaptured: store.effects.length, returned: items.length, effects: items };
163
+ }
164
+ function getEffectErrors(store) {
165
+ var errors = store.effects
166
+ .filter(function (m) { var _a; return ((_a = m.effectEvent) === null || _a === void 0 ? void 0 : _a.lifecycle) === 'error'; })
167
+ .map(function (m) {
168
+ var _a, _b, _c, _d;
169
+ return ({
170
+ effect: (_a = m.effectName) !== null && _a !== void 0 ? _a : (_b = m.effectEvent) === null || _b === void 0 ? void 0 : _b.name,
171
+ triggerAction: m.action,
172
+ timestamp: m.timestamp,
173
+ errorMessage: (_c = m.effectEvent) === null || _c === void 0 ? void 0 : _c.errorMessage,
174
+ errorStack: (_d = m.effectEvent) === null || _d === void 0 ? void 0 : _d.errorStack,
175
+ });
176
+ });
177
+ return { count: errors.length, errors: errors };
178
+ }
179
+ function getCurrentState(store) {
180
+ var last = store.actions[store.actions.length - 1];
181
+ if (!last)
182
+ return { state: null, note: 'No state captured yet.' };
183
+ return { afterAction: actionType(last), timestamp: last.timestamp, state: last.nextState };
184
+ }
185
+ function getStateDiff(store, target) {
186
+ var index = store.actions.length - 1;
187
+ if (target) {
188
+ var found = store.actions.map(actionType).lastIndexOf(target);
189
+ if (found >= 0)
190
+ index = found;
191
+ else
192
+ return { error: "No captured action of type \"".concat(target, "\".") };
193
+ }
194
+ if (index < 0)
195
+ return { error: 'No actions captured yet.' };
196
+ var current = store.actions[index];
197
+ var previous = index > 0 ? store.actions[index - 1] : undefined;
198
+ var changes = diffStates(previous === null || previous === void 0 ? void 0 : previous.nextState, current.nextState);
199
+ return {
200
+ action: actionType(current),
201
+ timestamp: current.timestamp,
202
+ changedPaths: changes.length,
203
+ changes: changes,
204
+ };
205
+ }
206
+ function getPerformanceStats(store) {
207
+ var times = store.actions
208
+ .map(function (m) { var _a, _b, _c; return ({ type: actionType(m), ms: (_b = (_a = m.renderTiming) === null || _a === void 0 ? void 0 : _a.totalTime) !== null && _b !== void 0 ? _b : (_c = m.renderPerformance) === null || _c === void 0 ? void 0 : _c.renderTime }); })
209
+ .filter(function (t) { return typeof t.ms === 'number'; });
210
+ if (times.length === 0)
211
+ return { note: 'No render timings captured yet.' };
212
+ var values = times.map(function (t) { return t.ms; });
213
+ var avg = values.reduce(function (a, b) { return a + b; }, 0) / values.length;
214
+ var max = Math.max.apply(Math, values);
215
+ var slowest = __spreadArray([], times, true).sort(function (a, b) { return b.ms - a.ms; }).slice(0, 5);
216
+ var overBudget = times.filter(function (t) { return t.ms > 16; }).length;
217
+ return {
218
+ measuredActions: times.length,
219
+ avgRenderMs: Number(avg.toFixed(2)),
220
+ maxRenderMs: Number(max.toFixed(2)),
221
+ overFrameBudget16ms: overBudget,
222
+ slowest: slowest,
223
+ };
224
+ }
225
+ function getDebugSummary(store) {
226
+ var errors = getEffectErrors(store);
227
+ var perf = getPerformanceStats(store);
228
+ var effectMap = buildEffectActionMap(store);
229
+ var recent = store.actions.slice(-10).map(function (m) {
230
+ var _a;
231
+ var type = actionType(m);
232
+ var effectName = (_a = m.effectName) !== null && _a !== void 0 ? _a : effectMap.get(type);
233
+ return effectName ? "".concat(type, " (via ").concat(effectName, ")") : type;
234
+ });
235
+ var effectExecutions = store.effects.filter(function (e) { var _a; return ((_a = e.effectEvent) === null || _a === void 0 ? void 0 : _a.lifecycle) === 'emitted'; }).length;
236
+ return {
237
+ appName: store.appName,
238
+ totalActions: store.actions.length,
239
+ totalEffectEvents: store.effects.length,
240
+ effectExecutions: effectExecutions,
241
+ effectErrorCount: errors.errors.length,
242
+ clearCount: store.clearCount,
243
+ lastClearedAt: store.lastClearedAt,
244
+ recentActions: recent,
245
+ effectErrors: errors.errors,
246
+ performance: perf,
247
+ hint: 'Use get_state_diff(actionType) to inspect what changed for a specific action, ' +
248
+ 'get_effects to see the effect chain, and get_current_state for the latest store snapshot.',
249
+ };
250
+ }
251
+ var TOOLS = [
252
+ {
253
+ name: 'get_debug_summary',
254
+ description: 'High-level health snapshot of the live NgRx session: recent actions, effect errors and render performance. Start here when debugging.',
255
+ inputSchema: { type: 'object', properties: {} },
256
+ run: function (store) { return getDebugSummary(store); },
257
+ },
258
+ {
259
+ name: 'get_action_timeline',
260
+ description: 'Recent dispatched actions with source (user/effect), render time and any performance warnings.',
261
+ inputSchema: {
262
+ type: 'object',
263
+ properties: { limit: { type: 'number', description: 'Max actions to return (default 25).' } },
264
+ },
265
+ run: function (store, args) { return getActionTimeline(store, clampLimit(args === null || args === void 0 ? void 0 : args.limit, 25)); },
266
+ },
267
+ {
268
+ name: 'get_effects',
269
+ description: 'Recent NgRx effect executions (lifecycle, duration, result action).',
270
+ inputSchema: {
271
+ type: 'object',
272
+ properties: { limit: { type: 'number', description: 'Max effect events to return (default 25).' } },
273
+ },
274
+ run: function (store, args) { return getEffects(store, clampLimit(args === null || args === void 0 ? void 0 : args.limit, 25)); },
275
+ },
276
+ {
277
+ name: 'get_effect_errors',
278
+ description: 'All captured effect errors with message and stack trace. Use when an effect is failing.',
279
+ inputSchema: { type: 'object', properties: {} },
280
+ run: function (store) { return getEffectErrors(store); },
281
+ },
282
+ {
283
+ name: 'get_state_diff',
284
+ description: 'What changed in the store for an action. Pass an actionType to target a specific action, or omit for the most recent.',
285
+ inputSchema: {
286
+ type: 'object',
287
+ properties: { actionType: { type: 'string', description: 'Action type to diff (optional).' } },
288
+ },
289
+ run: function (store, args) { return getStateDiff(store, typeof (args === null || args === void 0 ? void 0 : args.actionType) === 'string' ? args.actionType : undefined); },
290
+ },
291
+ {
292
+ name: 'get_current_state',
293
+ description: 'The latest full store snapshot captured by the DevTool.',
294
+ inputSchema: { type: 'object', properties: {} },
295
+ run: function (store) { return getCurrentState(store); },
296
+ },
297
+ {
298
+ name: 'get_performance_stats',
299
+ description: 'Render-timing aggregates: average/max render time and the slowest actions.',
300
+ inputSchema: { type: 'object', properties: {} },
301
+ run: function (store) { return getPerformanceStats(store); },
302
+ },
303
+ {
304
+ name: 'clear_debug_session',
305
+ description: 'Clear the MCP capture history for the current NgRx DevTool session.',
306
+ inputSchema: { type: 'object', properties: {} },
307
+ run: function (store) { return clearStore(store, 'mcp-tool'); },
308
+ },
309
+ ];
310
+ function clampLimit(value, fallback) {
311
+ var n = Number(value);
312
+ if (!Number.isFinite(n) || n <= 0)
313
+ return fallback;
314
+ return Math.min(Math.floor(n), MAX_ENTRIES);
315
+ }
316
+ function rpcResult(id, result) {
317
+ return { jsonrpc: '2.0', id: id, result: result };
318
+ }
319
+ function rpcError(id, code, message) {
320
+ return { jsonrpc: '2.0', id: id, error: { code: code, message: message } };
321
+ }
322
+ function handleRpc(rpc, store) {
323
+ if (!isRpcRequest(rpc)) {
324
+ return isIndexable(rpc) && isRpcId(rpc.id) ? rpcError(rpc.id, -32600, 'Invalid Request') : null;
325
+ }
326
+ var id = rpc.id, method = rpc.method, params = rpc.params;
327
+ var isNotification = id === undefined || id === null;
328
+ switch (method) {
329
+ case 'initialize':
330
+ return rpcResult(id, {
331
+ protocolVersion: typeof (params === null || params === void 0 ? void 0 : params.protocolVersion) === 'string' ? params.protocolVersion : MCP_PROTOCOL_VERSION,
332
+ capabilities: { tools: {} },
333
+ serverInfo: { name: 'ngrx-devtool', version: '1.0.0' },
334
+ instructions: 'Query the live NgRx DevTool session. Call get_debug_summary first, then drill in with the other tools.',
335
+ });
336
+ case 'notifications/initialized':
337
+ case 'notifications/cancelled':
338
+ return null;
339
+ case 'ping':
340
+ return isNotification ? null : rpcResult(id, {});
341
+ case 'tools/list':
342
+ return rpcResult(id, {
343
+ tools: TOOLS.map(function (t) { return ({ name: t.name, description: t.description, inputSchema: t.inputSchema }); }),
344
+ });
345
+ case 'tools/call': {
346
+ var tool = TOOLS.find(function (t) { return t.name === (params === null || params === void 0 ? void 0 : params.name); });
347
+ if (!tool)
348
+ return rpcError(id, -32602, "Unknown tool: ".concat(params === null || params === void 0 ? void 0 : params.name));
349
+ try {
350
+ var args = isIndexable(params === null || params === void 0 ? void 0 : params.arguments) ? params.arguments : {};
351
+ var data = tool.run(store, args);
352
+ return rpcResult(id, {
353
+ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
354
+ });
355
+ }
356
+ catch (err) {
357
+ return rpcResult(id, {
358
+ content: [{ type: 'text', text: "Tool error: ".concat(err instanceof Error ? err.message : String(err)) }],
359
+ isError: true,
360
+ });
361
+ }
362
+ }
363
+ default:
364
+ return isNotification ? null : rpcError(id, -32601, "Method not found: ".concat(method));
365
+ }
366
+ }
367
+ function registerMcpEndpoint(app, express, store) {
368
+ var jsonBody = express.json({ limit: '8mb' });
369
+ var setCors = function (res) {
370
+ res.setHeader('Access-Control-Allow-Origin', '*');
371
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
372
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Mcp-Session-Id, mcp-protocol-version');
373
+ };
374
+ app.options('/mcp', function (_req, res) {
375
+ setCors(res);
376
+ res.status(204).end();
377
+ });
378
+ app.post('/mcp', jsonBody, function (req, res) {
379
+ setCors(res);
380
+ var body = req.body;
381
+ try {
382
+ if (Array.isArray(body)) {
383
+ var responses = body.map(function (rpc) { return handleRpc(rpc, store); }).filter(function (r) { return r !== null; });
384
+ if (responses.length === 0)
385
+ return res.status(202).end();
386
+ return res.json(responses);
387
+ }
388
+ var response = handleRpc(body, store);
389
+ if (response === null)
390
+ return res.status(202).end();
391
+ return res.json(response);
392
+ }
393
+ catch (err) {
394
+ var id = isIndexable(body) && isRpcId(body.id) ? body.id : null;
395
+ return res.status(500).json(rpcError(id, -32603, err instanceof Error ? err.message : 'Internal error'));
396
+ }
397
+ });
398
+ app.get('/mcp', function (_req, res) {
399
+ setCors(res);
400
+ res.setHeader('Allow', 'POST, OPTIONS');
401
+ res.status(405).json(rpcError(null, -32000, 'Method Not Allowed: this server only supports POST /mcp'));
402
+ });
403
+ app.delete('/mcp', function (_req, res) {
404
+ setCors(res);
405
+ clearStore(store, 'mcp-delete');
406
+ res.status(200).end();
407
+ });
408
+ }
409
+ function isIndexable(value) {
410
+ return value !== null && typeof value === 'object';
411
+ }
412
+ function isRpcId(value) {
413
+ return value === null || typeof value === 'string' || typeof value === 'number';
414
+ }
415
+ function isRpcRequest(value) {
416
+ return (isIndexable(value) &&
417
+ value.jsonrpc === '2.0' &&
418
+ typeof value.method === 'string' &&
419
+ (value.id === undefined || isRpcId(value.id)) &&
420
+ (value.params === undefined || isIndexable(value.params)));
421
+ }
422
+ function isCapturedMessage(value) {
423
+ return isIndexable(value) && typeof value.type === 'string';
424
+ }
@@ -336,6 +336,10 @@ class ActionsInterceptorService {
336
336
  }
337
337
  setupEffectEventForwarding() {
338
338
  this.effectTracker.effectEvents$.pipe(takeUntil$1(this.destroy$), tap((event) => {
339
+ const errorMessage = event.error instanceof Error
340
+ ? event.error.message
341
+ : event.error != null ? String(event.error) : undefined;
342
+ const errorStack = event.error instanceof Error ? event.error.stack : undefined;
339
343
  const message = {
340
344
  type: 'EFFECT_EVENT',
341
345
  action: event.action?.type,
@@ -346,6 +350,8 @@ class ActionsInterceptorService {
346
350
  duration: event.duration,
347
351
  executionId: event.executionId,
348
352
  dispatch: event.dispatch,
353
+ errorMessage,
354
+ errorStack,
349
355
  },
350
356
  timestamp: new Date().toISOString(),
351
357
  };
@@ -555,18 +561,23 @@ class PerformanceTrackerService {
555
561
  thresholds = DEFAULT_THRESHOLDS;
556
562
  firstActionTime = null;
557
563
  measureRenderTime(actionType, reducer, callback) {
558
- const startTime = performance.now();
564
+ const renderStartTime = performance.now();
565
+ const reducerStartTime = performance.now();
559
566
  const nextState = reducer();
567
+ const reducerTime = parseFloat((performance.now() - reducerStartTime).toFixed(2));
568
+ const stateSize = this.getSerializedSize(nextState);
560
569
  if (!this.isBrowser) {
561
- callback(0);
570
+ callback(0, reducerTime, stateSize);
562
571
  return nextState;
563
572
  }
564
573
  afterNextRender(() => {
565
- const renderTime = parseFloat((performance.now() - startTime).toFixed(2));
574
+ const renderTime = parseFloat((performance.now() - renderStartTime).toFixed(2));
566
575
  const entry = {
567
576
  actionType,
568
577
  timestamp: Date.now(),
569
578
  renderTime,
579
+ reducerTime,
580
+ stateSize,
570
581
  };
571
582
  this.entries.push(entry);
572
583
  if (this.firstActionTime === null) {
@@ -575,7 +586,7 @@ class PerformanceTrackerService {
575
586
  if (this.entries.length > ENTRIES_MAX_SIZE) {
576
587
  this.entries = this.entries.slice(-ENTRIES_TRIM_SIZE);
577
588
  }
578
- callback(renderTime);
589
+ callback(renderTime, reducerTime, stateSize);
579
590
  }, { injector: this.injector });
580
591
  return nextState;
581
592
  }
@@ -605,17 +616,26 @@ class PerformanceTrackerService {
605
616
  getAggregatedStats() {
606
617
  const stats = this.getStats();
607
618
  const actionTypeStats = this.getActionTypeStats();
619
+ const reducerTimes = this.entries.map(entry => entry.reducerTime ?? entry.renderTime);
620
+ const avgReducerTime = reducerTimes.length
621
+ ? reducerTimes.reduce((total, time) => total + time, 0) / reducerTimes.length
622
+ : 0;
623
+ const maxReducerTime = reducerTimes.length ? Math.max(...reducerTimes) : 0;
624
+ const slowestReducer = this.entries.reduce((slowest, entry) => !slowest || (entry.reducerTime ?? entry.renderTime) > (slowest.reducerTime ?? slowest.renderTime)
625
+ ? entry
626
+ : slowest, null);
627
+ const currentStateSize = this.entries.at(-1)?.stateSize ?? 0;
608
628
  const elapsedTime = this.firstActionTime
609
629
  ? (Date.now() - this.firstActionTime) / 1000
610
630
  : 1;
611
631
  return {
612
632
  totalActions: stats.totalActions,
613
- avgReducerTime: stats.avgRenderTime,
614
- maxReducerTime: stats.maxRenderTime,
615
- slowestAction: stats.slowestAction,
616
- currentStateSize: 0,
633
+ avgReducerTime,
634
+ maxReducerTime,
635
+ slowestAction: slowestReducer?.actionType ?? null,
636
+ currentStateSize,
617
637
  actionsPerSecond: stats.totalActions / Math.max(elapsedTime, 1),
618
- performanceScore: this.calculatePerformanceScore(stats),
638
+ performanceScore: this.calculatePerformanceScore(avgReducerTime, maxReducerTime),
619
639
  actionTypeStats,
620
640
  };
621
641
  }
@@ -659,36 +679,46 @@ class PerformanceTrackerService {
659
679
  getActionTypeStats() {
660
680
  const statsMap = new Map();
661
681
  for (const entry of this.entries) {
682
+ const reducerTime = entry.reducerTime ?? entry.renderTime;
662
683
  const existing = statsMap.get(entry.actionType);
663
684
  if (existing) {
664
685
  existing.count++;
665
- existing.totalTime += entry.renderTime;
686
+ existing.totalTime += reducerTime;
666
687
  existing.avgTime = existing.totalTime / existing.count;
667
- existing.maxTime = Math.max(existing.maxTime, entry.renderTime);
688
+ existing.maxTime = Math.max(existing.maxTime, reducerTime);
668
689
  existing.lastExecuted = entry.timestamp;
669
690
  }
670
691
  else {
671
692
  statsMap.set(entry.actionType, {
672
693
  count: 1,
673
- totalTime: entry.renderTime,
674
- avgTime: entry.renderTime,
675
- maxTime: entry.renderTime,
694
+ totalTime: reducerTime,
695
+ avgTime: reducerTime,
696
+ maxTime: reducerTime,
676
697
  lastExecuted: entry.timestamp,
677
698
  });
678
699
  }
679
700
  }
680
701
  return statsMap;
681
702
  }
682
- calculatePerformanceScore(stats) {
703
+ calculatePerformanceScore(avgReducerTime, maxReducerTime) {
683
704
  let score = 100;
684
- if (stats.avgRenderTime > this.thresholds.maxReducerTime) {
685
- score -= Math.min(30, (stats.avgRenderTime - this.thresholds.maxReducerTime) * 2);
705
+ if (avgReducerTime > this.thresholds.maxReducerTime) {
706
+ score -= Math.min(30, (avgReducerTime - this.thresholds.maxReducerTime) * 2);
686
707
  }
687
- if (stats.maxRenderTime > this.thresholds.maxReducerTime * 2) {
688
- score -= Math.min(20, (stats.maxRenderTime - this.thresholds.maxReducerTime * 2) / 2);
708
+ if (maxReducerTime > this.thresholds.maxReducerTime * 2) {
709
+ score -= Math.min(20, (maxReducerTime - this.thresholds.maxReducerTime * 2) / 2);
689
710
  }
690
711
  return Math.max(0, Math.round(score));
691
712
  }
713
+ getSerializedSize(value) {
714
+ try {
715
+ const serialized = JSON.stringify(value);
716
+ return serialized === undefined ? 0 : new TextEncoder().encode(serialized).byteLength;
717
+ }
718
+ catch {
719
+ return 0;
720
+ }
721
+ }
692
722
  maxSeverity(a, b) {
693
723
  const order = { low: 0, medium: 1, high: 2 };
694
724
  return order[a] >= order[b] ? a : b;
@@ -1324,14 +1354,14 @@ function createDevToolMetaReducer(wsUrlOrConfig = DEFAULT_WS_URL) {
1324
1354
  const timestamp = new Date().toISOString();
1325
1355
  let nextState;
1326
1356
  if (enablePerf) {
1327
- nextState = performanceTracker.measureRenderTime(action.type, () => reducer(state, action), (renderTime) => {
1357
+ nextState = performanceTracker.measureRenderTime(action.type, () => reducer(state, action), (renderTime, reducerTime, stateSize) => {
1328
1358
  const message = {
1329
1359
  type: 'STATE_CHANGE',
1330
1360
  action,
1331
1361
  prevState,
1332
1362
  nextState,
1333
1363
  timestamp,
1334
- renderPerformance: { renderTime }
1364
+ renderPerformance: { renderTime, reducerTime, stateSize }
1335
1365
  };
1336
1366
  webSocketService.send(message);
1337
1367
  });