@amadeus-it-group/ngrx-devtool 0.2.2 → 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/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
  };