swagger-parser-mcp-server 2.0.4 → 2.3.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 (131) hide show
  1. package/README.md +131 -100
  2. package/dist/application/snapshot/createSnapshotRuntime.d.ts +2 -0
  3. package/dist/application/snapshot/createSnapshotRuntime.d.ts.map +1 -0
  4. package/dist/application/snapshot/createSnapshotRuntime.js +2 -0
  5. package/dist/application/snapshot/createSnapshotRuntime.js.map +1 -0
  6. package/dist/application/snapshot/snapshotCaptureService.d.ts +90 -0
  7. package/dist/application/snapshot/snapshotCaptureService.d.ts.map +1 -0
  8. package/dist/application/snapshot/snapshotCaptureService.js +394 -0
  9. package/dist/application/snapshot/snapshotCaptureService.js.map +1 -0
  10. package/dist/application/snapshot/snapshotRepository.d.ts +77 -0
  11. package/dist/application/snapshot/snapshotRepository.d.ts.map +1 -0
  12. package/dist/application/snapshot/snapshotRepository.js +2 -0
  13. package/dist/application/snapshot/snapshotRepository.js.map +1 -0
  14. package/dist/domain/canonical/canonicalSnapshot.d.ts +61 -0
  15. package/dist/domain/canonical/canonicalSnapshot.d.ts.map +1 -0
  16. package/dist/domain/canonical/canonicalSnapshot.js +300 -0
  17. package/dist/domain/canonical/canonicalSnapshot.js.map +1 -0
  18. package/dist/domain/contracts/runtimeEnvironmentContract.d.ts +21 -0
  19. package/dist/domain/contracts/runtimeEnvironmentContract.d.ts.map +1 -0
  20. package/dist/domain/contracts/runtimeEnvironmentContract.js +50 -0
  21. package/dist/domain/contracts/runtimeEnvironmentContract.js.map +1 -0
  22. package/dist/domain/contracts/snapshotDiffContract.d.ts +270 -0
  23. package/dist/domain/contracts/snapshotDiffContract.d.ts.map +1 -0
  24. package/dist/domain/contracts/snapshotDiffContract.js +99 -0
  25. package/dist/domain/contracts/snapshotDiffContract.js.map +1 -0
  26. package/dist/domain/diff/endpointDiffClassifier.d.ts +78 -0
  27. package/dist/domain/diff/endpointDiffClassifier.d.ts.map +1 -0
  28. package/dist/domain/diff/endpointDiffClassifier.js +317 -0
  29. package/dist/domain/diff/endpointDiffClassifier.js.map +1 -0
  30. package/dist/http.d.ts +3 -0
  31. package/dist/http.d.ts.map +1 -0
  32. package/dist/http.js +52 -0
  33. package/dist/http.js.map +1 -0
  34. package/dist/index.d.ts +2 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +88 -266
  37. package/dist/index.js.map +1 -1
  38. package/dist/infrastructure/postgres/migrationRunner.d.ts +14 -0
  39. package/dist/infrastructure/postgres/migrationRunner.d.ts.map +1 -0
  40. package/dist/infrastructure/postgres/migrationRunner.js +161 -0
  41. package/dist/infrastructure/postgres/migrationRunner.js.map +1 -0
  42. package/dist/infrastructure/postgres/migrations/001_snapshot_schema.sql +29 -0
  43. package/dist/infrastructure/postgres/migrations/002_snapshot_change_history.sql +32 -0
  44. package/dist/infrastructure/postgres/postgresSnapshotRepository.d.ts +25 -0
  45. package/dist/infrastructure/postgres/postgresSnapshotRepository.d.ts.map +1 -0
  46. package/dist/infrastructure/postgres/postgresSnapshotRepository.js +323 -0
  47. package/dist/infrastructure/postgres/postgresSnapshotRepository.js.map +1 -0
  48. package/dist/infrastructure/postgres/runMigrations.d.ts +3 -0
  49. package/dist/infrastructure/postgres/runMigrations.d.ts.map +1 -0
  50. package/dist/infrastructure/postgres/runMigrations.js +33 -0
  51. package/dist/infrastructure/postgres/runMigrations.js.map +1 -0
  52. package/dist/infrastructure/runtime/createSnapshotRuntime.d.ts +17 -0
  53. package/dist/infrastructure/runtime/createSnapshotRuntime.d.ts.map +1 -0
  54. package/dist/infrastructure/runtime/createSnapshotRuntime.js +38 -0
  55. package/dist/infrastructure/runtime/createSnapshotRuntime.js.map +1 -0
  56. package/dist/schemas.d.ts +284 -3
  57. package/dist/schemas.d.ts.map +1 -1
  58. package/dist/schemas.js +110 -1
  59. package/dist/schemas.js.map +1 -1
  60. package/dist/tests/canonicalSnapshot.test.d.ts +2 -0
  61. package/dist/tests/canonicalSnapshot.test.d.ts.map +1 -0
  62. package/dist/tests/canonicalSnapshot.test.js +425 -0
  63. package/dist/tests/canonicalSnapshot.test.js.map +1 -0
  64. package/dist/tests/endpointDiffClassifier.test.d.ts +2 -0
  65. package/dist/tests/endpointDiffClassifier.test.d.ts.map +1 -0
  66. package/dist/tests/endpointDiffClassifier.test.js +633 -0
  67. package/dist/tests/endpointDiffClassifier.test.js.map +1 -0
  68. package/dist/tests/httpSnapshotTransport.test.d.ts +2 -0
  69. package/dist/tests/httpSnapshotTransport.test.d.ts.map +1 -0
  70. package/dist/tests/httpSnapshotTransport.test.js +356 -0
  71. package/dist/tests/httpSnapshotTransport.test.js.map +1 -0
  72. package/dist/tests/indexLifecycle.test.d.ts +2 -0
  73. package/dist/tests/indexLifecycle.test.d.ts.map +1 -0
  74. package/dist/tests/indexLifecycle.test.js +23 -0
  75. package/dist/tests/indexLifecycle.test.js.map +1 -0
  76. package/dist/tests/mcpSnapshotTools.test.d.ts +2 -0
  77. package/dist/tests/mcpSnapshotTools.test.d.ts.map +1 -0
  78. package/dist/tests/mcpSnapshotTools.test.js +316 -0
  79. package/dist/tests/mcpSnapshotTools.test.js.map +1 -0
  80. package/dist/tests/postgresMigrationSmoke.test.d.ts +2 -0
  81. package/dist/tests/postgresMigrationSmoke.test.d.ts.map +1 -0
  82. package/dist/tests/postgresMigrationSmoke.test.js +187 -0
  83. package/dist/tests/postgresMigrationSmoke.test.js.map +1 -0
  84. package/dist/tests/realPostgresTestSchema.d.ts +10 -0
  85. package/dist/tests/realPostgresTestSchema.d.ts.map +1 -0
  86. package/dist/tests/realPostgresTestSchema.js +73 -0
  87. package/dist/tests/realPostgresTestSchema.js.map +1 -0
  88. package/dist/tests/snapshotCapturePipeline.test.d.ts +2 -0
  89. package/dist/tests/snapshotCapturePipeline.test.d.ts.map +1 -0
  90. package/dist/tests/snapshotCapturePipeline.test.js +475 -0
  91. package/dist/tests/snapshotCapturePipeline.test.js.map +1 -0
  92. package/dist/tests/snapshotDiffContract.test.d.ts +2 -0
  93. package/dist/tests/snapshotDiffContract.test.d.ts.map +1 -0
  94. package/dist/tests/snapshotDiffContract.test.js +156 -0
  95. package/dist/tests/snapshotDiffContract.test.js.map +1 -0
  96. package/dist/tests/snapshotPersistence.real.test.d.ts +2 -0
  97. package/dist/tests/snapshotPersistence.real.test.d.ts.map +1 -0
  98. package/dist/tests/snapshotPersistence.real.test.js +310 -0
  99. package/dist/tests/snapshotPersistence.real.test.js.map +1 -0
  100. package/dist/tests/webServerRuntime.test.d.ts +2 -0
  101. package/dist/tests/webServerRuntime.test.d.ts.map +1 -0
  102. package/dist/tests/webServerRuntime.test.js +123 -0
  103. package/dist/tests/webServerRuntime.test.js.map +1 -0
  104. package/dist/transport/createSnapshotToolRuntime.d.ts +7 -0
  105. package/dist/transport/createSnapshotToolRuntime.d.ts.map +1 -0
  106. package/dist/transport/createSnapshotToolRuntime.js +40 -0
  107. package/dist/transport/createSnapshotToolRuntime.js.map +1 -0
  108. package/dist/transport/httpSnapshotServer.d.ts +11 -0
  109. package/dist/transport/httpSnapshotServer.d.ts.map +1 -0
  110. package/dist/transport/httpSnapshotServer.js +216 -0
  111. package/dist/transport/httpSnapshotServer.js.map +1 -0
  112. package/dist/transport/mcpToolRouter.d.ts +81 -0
  113. package/dist/transport/mcpToolRouter.d.ts.map +1 -0
  114. package/dist/transport/mcpToolRouter.js +416 -0
  115. package/dist/transport/mcpToolRouter.js.map +1 -0
  116. package/dist/transport/webServerRuntime.d.ts +17 -0
  117. package/dist/transport/webServerRuntime.d.ts.map +1 -0
  118. package/dist/transport/webServerRuntime.js +73 -0
  119. package/dist/transport/webServerRuntime.js.map +1 -0
  120. package/dist/utils/swaggerCache.d.ts +4 -0
  121. package/dist/utils/swaggerCache.d.ts.map +1 -1
  122. package/dist/utils/swaggerCache.js +8 -0
  123. package/dist/utils/swaggerCache.js.map +1 -1
  124. package/dist/utils/types.d.ts +2 -1
  125. package/dist/utils/types.d.ts.map +1 -1
  126. package/dist/utils/types.js +2 -0
  127. package/dist/utils/types.js.map +1 -1
  128. package/dist/web/dashboard.css +411 -0
  129. package/dist/web/dashboard.html +141 -0
  130. package/dist/web/dashboard.js +540 -0
  131. package/package.json +27 -17
@@ -0,0 +1,540 @@
1
+ const API_BASE = '/api/snapshots';
2
+ const HISTORY_LIMIT = 50;
3
+
4
+ const elements = {
5
+ alert: document.getElementById('status-alert'),
6
+ sourceCaption: document.getElementById('source-caption'),
7
+ refreshButton: document.getElementById('refresh-snapshots-button'),
8
+ captureButton: document.getElementById('capture-button'),
9
+ compareForm: document.getElementById('compare-form'),
10
+ baselineSelect: document.getElementById('baseline-select'),
11
+ targetSelect: document.getElementById('target-select'),
12
+ snapshotList: document.getElementById('snapshot-list'),
13
+ metricTotal: document.getElementById('metric-total'),
14
+ metricBreaking: document.getElementById('metric-breaking'),
15
+ metricNonBreaking: document.getElementById('metric-non-breaking'),
16
+ metricInfo: document.getElementById('metric-info'),
17
+ diffSummaryLine: document.getElementById('diff-summary-line'),
18
+ changeTableBody: document.getElementById('change-table-body'),
19
+ historyCaption: document.getElementById('history-caption'),
20
+ historyFilterForm: document.getElementById('history-filter-form'),
21
+ historyPathInput: document.getElementById('history-path-input'),
22
+ historyMethodSelect: document.getElementById('history-method-select'),
23
+ historyClearButton: document.getElementById('history-clear-button'),
24
+ historyList: document.getElementById('history-list'),
25
+ detailHeading: document.getElementById('detail-heading'),
26
+ detailEvents: document.getElementById('detail-events'),
27
+ detailBefore: document.getElementById('detail-before'),
28
+ detailAfter: document.getElementById('detail-after'),
29
+ };
30
+
31
+ if (
32
+ !elements.alert ||
33
+ !elements.sourceCaption ||
34
+ !elements.refreshButton ||
35
+ !elements.captureButton ||
36
+ !elements.compareForm ||
37
+ !elements.baselineSelect ||
38
+ !elements.targetSelect ||
39
+ !elements.snapshotList ||
40
+ !elements.metricTotal ||
41
+ !elements.metricBreaking ||
42
+ !elements.metricNonBreaking ||
43
+ !elements.metricInfo ||
44
+ !elements.diffSummaryLine ||
45
+ !elements.changeTableBody ||
46
+ !elements.historyCaption ||
47
+ !elements.historyFilterForm ||
48
+ !elements.historyPathInput ||
49
+ !elements.historyMethodSelect ||
50
+ !elements.historyClearButton ||
51
+ !elements.historyList ||
52
+ !elements.detailHeading ||
53
+ !elements.detailEvents ||
54
+ !elements.detailBefore ||
55
+ !elements.detailAfter
56
+ ) {
57
+ throw new Error('Dashboard markup is missing required elements.');
58
+ }
59
+
60
+ const state = {
61
+ projectKey: '',
62
+ sourceUrl: '',
63
+ snapshots: [],
64
+ diffResult: null,
65
+ historyEvents: [],
66
+ historyFilter: {
67
+ path: null,
68
+ method: null,
69
+ },
70
+ };
71
+
72
+ function formatTimestamp(isoDate) {
73
+ const parsed = new Date(isoDate);
74
+ return Number.isNaN(parsed.getTime()) ? isoDate : parsed.toLocaleString();
75
+ }
76
+
77
+ function setAlert(message, kind = 'info') {
78
+ if (!message) {
79
+ elements.alert.hidden = true;
80
+ elements.alert.textContent = '';
81
+ elements.alert.classList.remove('error');
82
+ return;
83
+ }
84
+
85
+ elements.alert.hidden = false;
86
+ elements.alert.textContent = message;
87
+ elements.alert.classList.toggle('error', kind === 'error');
88
+ }
89
+
90
+ async function requestJson(path, options) {
91
+ const response = await fetch(path, options);
92
+ let payload = null;
93
+ try {
94
+ payload = await response.json();
95
+ } catch {
96
+ payload = null;
97
+ }
98
+
99
+ if (!response.ok) {
100
+ const errorMessage =
101
+ payload && typeof payload.error === 'string' ? payload.error : `Request failed: ${response.status}`;
102
+ throw new Error(errorMessage);
103
+ }
104
+
105
+ return payload;
106
+ }
107
+
108
+ function renderSnapshotSelects() {
109
+ const previousBaseline = elements.baselineSelect.value;
110
+ const previousTarget = elements.targetSelect.value;
111
+ const options = state.snapshots
112
+ .map(snapshot => `<option value="${snapshot.id}">${formatTimestamp(snapshot.capturedAt)} · ${snapshot.id}</option>`)
113
+ .join('');
114
+
115
+ elements.baselineSelect.innerHTML = options;
116
+ elements.targetSelect.innerHTML = options;
117
+
118
+ const hasPreviousBaseline = state.snapshots.some(snapshot => snapshot.id === previousBaseline);
119
+ const hasPreviousTarget = state.snapshots.some(snapshot => snapshot.id === previousTarget);
120
+
121
+ if (hasPreviousBaseline) {
122
+ elements.baselineSelect.value = previousBaseline;
123
+ }
124
+ if (hasPreviousTarget) {
125
+ elements.targetSelect.value = previousTarget;
126
+ }
127
+
128
+ if (!elements.baselineSelect.value && state.snapshots.length >= 2) {
129
+ elements.baselineSelect.value = state.snapshots[1].id;
130
+ }
131
+
132
+ if (!elements.targetSelect.value && state.snapshots.length >= 1) {
133
+ elements.targetSelect.value = state.snapshots[0].id;
134
+ }
135
+
136
+ if (elements.baselineSelect.value === elements.targetSelect.value && state.snapshots.length >= 2) {
137
+ elements.baselineSelect.value = state.snapshots[1].id;
138
+ elements.targetSelect.value = state.snapshots[0].id;
139
+ }
140
+ }
141
+
142
+ function renderSnapshotList() {
143
+ if (state.snapshots.length === 0) {
144
+ elements.snapshotList.innerHTML = '<p class="caption">No snapshots captured yet.</p>';
145
+ return;
146
+ }
147
+
148
+ const list = document.createElement('div');
149
+ list.className = 'snapshot-list';
150
+
151
+ for (const snapshot of state.snapshots) {
152
+ const button = document.createElement('button');
153
+ button.type = 'button';
154
+ button.className = 'snapshot-item';
155
+ button.innerHTML = `
156
+ <time datetime="${snapshot.capturedAt}">${formatTimestamp(snapshot.capturedAt)}</time>
157
+ <strong>${snapshot.id}</strong>
158
+ `;
159
+ button.addEventListener('click', () => {
160
+ elements.targetSelect.value = snapshot.id;
161
+ if (elements.baselineSelect.value === snapshot.id && state.snapshots.length > 1) {
162
+ const fallback = state.snapshots.find(item => item.id !== snapshot.id);
163
+ if (fallback) {
164
+ elements.baselineSelect.value = fallback.id;
165
+ }
166
+ }
167
+ setAlert(`Target snapshot selected: ${snapshot.id}`);
168
+ });
169
+ list.appendChild(button);
170
+ }
171
+
172
+ elements.snapshotList.innerHTML = '';
173
+ elements.snapshotList.appendChild(list);
174
+ }
175
+
176
+ function renderChangeHistory() {
177
+ elements.historyPathInput.value = state.historyFilter.path ?? '';
178
+ elements.historyMethodSelect.value = state.historyFilter.method ?? '';
179
+
180
+ if (state.historyFilter.path && state.historyFilter.method) {
181
+ elements.historyCaption.textContent = `Timeline for ${state.historyFilter.method.toUpperCase()} ${state.historyFilter.path}`;
182
+ } else {
183
+ elements.historyCaption.textContent = 'Latest endpoint changes.';
184
+ }
185
+
186
+ if (state.historyEvents.length === 0) {
187
+ elements.historyList.innerHTML = '<p class="caption">No change history events.</p>';
188
+ return;
189
+ }
190
+
191
+ const list = document.createElement('div');
192
+ list.className = 'history-list';
193
+
194
+ for (const event of state.historyEvents) {
195
+ const button = document.createElement('button');
196
+ button.type = 'button';
197
+ button.className = 'history-item';
198
+ button.innerHTML = `
199
+ <div class="history-item-top">
200
+ <strong>${event.method.toUpperCase()} ${event.path}</strong>
201
+ <span class="${classificationClassName(event.classification)}">${event.classification}</span>
202
+ </div>
203
+ <div class="history-item-meta">
204
+ <span>${event.changeReason}</span>
205
+ <time datetime="${event.changedAt}">${formatTimestamp(event.changedAt)}</time>
206
+ </div>
207
+ <div class="caption">${event.baselineSnapshotId} -> ${event.targetSnapshotId}</div>
208
+ `;
209
+ button.addEventListener('click', () => {
210
+ setLoading(true);
211
+ void fetchSnapshots()
212
+ .then(() => {
213
+ elements.baselineSelect.value = event.baselineSnapshotId;
214
+ elements.targetSelect.value = event.targetSnapshotId;
215
+ return compareSnapshots();
216
+ })
217
+ .then(() => loadEndpointDetail(event.path, event.method))
218
+ .then(() => {
219
+ setAlert(`History event loaded: ${event.method.toUpperCase()} ${event.path}`);
220
+ })
221
+ .catch(error => {
222
+ const message = error instanceof Error ? error.message : String(error);
223
+ setAlert(message, 'error');
224
+ })
225
+ .finally(() => {
226
+ setLoading(false);
227
+ });
228
+ });
229
+ list.appendChild(button);
230
+ }
231
+
232
+ elements.historyList.innerHTML = '';
233
+ elements.historyList.appendChild(list);
234
+ }
235
+
236
+ function classificationClassName(value) {
237
+ if (value === 'breaking') {
238
+ return 'classification breaking';
239
+ }
240
+ if (value === 'non-breaking') {
241
+ return 'classification non-breaking';
242
+ }
243
+ return 'classification info';
244
+ }
245
+
246
+ function resetDetail() {
247
+ elements.detailHeading.textContent = 'Pick one endpoint from the diff table.';
248
+ elements.detailEvents.innerHTML = '';
249
+ elements.detailBefore.textContent = 'No selection';
250
+ elements.detailAfter.textContent = 'No selection';
251
+ }
252
+
253
+ function renderDiffSummary() {
254
+ if (!state.diffResult) {
255
+ elements.metricTotal.textContent = '-';
256
+ elements.metricBreaking.textContent = '-';
257
+ elements.metricNonBreaking.textContent = '-';
258
+ elements.metricInfo.textContent = '-';
259
+ elements.diffSummaryLine.textContent = 'No diff selected.';
260
+ elements.changeTableBody.innerHTML = '<tr><td colspan="3" class="caption">No change rows.</td></tr>';
261
+ resetDetail();
262
+ return;
263
+ }
264
+
265
+ const { summary, counts, changes, warnings } = state.diffResult;
266
+ elements.metricTotal.textContent = String(counts.total);
267
+ elements.metricBreaking.textContent = String(counts.breaking);
268
+ elements.metricNonBreaking.textContent = String(counts.nonBreaking);
269
+ elements.metricInfo.textContent = String(counts.info);
270
+ elements.diffSummaryLine.textContent = `Compared ${summary.baselineSnapshotId} -> ${summary.targetSnapshotId} at ${formatTimestamp(
271
+ summary.comparedAt
272
+ )}`;
273
+
274
+ if (warnings.length > 0) {
275
+ setAlert(warnings.map(warning => warning.message).join(' | '));
276
+ }
277
+
278
+ if (changes.length === 0) {
279
+ elements.changeTableBody.innerHTML = '<tr><td colspan="3" class="caption">No endpoint changes.</td></tr>';
280
+ return;
281
+ }
282
+
283
+ elements.changeTableBody.innerHTML = '';
284
+ for (const change of changes) {
285
+ const row = document.createElement('tr');
286
+ const endpointCell = document.createElement('td');
287
+ const endpointButton = document.createElement('button');
288
+ endpointButton.type = 'button';
289
+ endpointButton.className = 'button secondary';
290
+ endpointButton.textContent = `${change.method.toUpperCase()} ${change.url}`;
291
+ endpointButton.addEventListener('click', () => {
292
+ void loadEndpointDetail(change.url, change.method);
293
+ });
294
+ endpointCell.appendChild(endpointButton);
295
+
296
+ const classificationCell = document.createElement('td');
297
+ classificationCell.className = classificationClassName(change.classification);
298
+ classificationCell.textContent = change.classification;
299
+
300
+ const reasonCell = document.createElement('td');
301
+ reasonCell.textContent = change.changeReason;
302
+
303
+ row.appendChild(endpointCell);
304
+ row.appendChild(classificationCell);
305
+ row.appendChild(reasonCell);
306
+ elements.changeTableBody.appendChild(row);
307
+ }
308
+ }
309
+
310
+ function renderEndpointDetail(detail) {
311
+ elements.detailHeading.textContent = `${detail.method.toUpperCase()} ${detail.path} · ${detail.changeReason ?? 'no_change'}`;
312
+ elements.detailEvents.innerHTML = '';
313
+
314
+ if (detail.details.length === 0) {
315
+ const empty = document.createElement('p');
316
+ empty.className = 'caption';
317
+ empty.textContent = 'No detail events.';
318
+ elements.detailEvents.appendChild(empty);
319
+ } else {
320
+ for (const event of detail.details) {
321
+ const item = document.createElement('div');
322
+ item.className = 'event-row';
323
+ item.innerHTML = `
324
+ <strong class="${classificationClassName(event.classification)}">${event.classification}</strong>
325
+ <div>${event.message}</div>
326
+ `;
327
+ elements.detailEvents.appendChild(item);
328
+ }
329
+ }
330
+
331
+ elements.detailBefore.textContent = JSON.stringify(detail.before, null, 2) ?? 'null';
332
+ elements.detailAfter.textContent = JSON.stringify(detail.after, null, 2) ?? 'null';
333
+ }
334
+
335
+ async function fetchSnapshots() {
336
+ const result = await requestJson(`${API_BASE}?limit=200`);
337
+ state.projectKey = result.projectKey;
338
+ state.sourceUrl = result.sourceUrl;
339
+ state.snapshots = result.snapshots;
340
+ elements.sourceCaption.textContent = `${result.projectKey} · ${result.sourceUrl}`;
341
+ renderSnapshotSelects();
342
+ renderSnapshotList();
343
+ }
344
+
345
+ function normalizeHistoryFilter(filter = {}) {
346
+ const path = typeof filter.path === 'string' && filter.path.trim().length > 0 ? filter.path.trim() : undefined;
347
+ const method =
348
+ typeof filter.method === 'string' && filter.method.trim().length > 0
349
+ ? filter.method.trim().toLowerCase()
350
+ : undefined;
351
+
352
+ if (!path && !method) {
353
+ return {};
354
+ }
355
+
356
+ return {
357
+ path,
358
+ method,
359
+ };
360
+ }
361
+
362
+ function readHistoryFilterInputs() {
363
+ return normalizeHistoryFilter({
364
+ path: elements.historyPathInput.value,
365
+ method: elements.historyMethodSelect.value,
366
+ });
367
+ }
368
+
369
+ async function loadChangeHistory(filter = {}) {
370
+ const normalizedFilter = normalizeHistoryFilter(filter);
371
+ const params = new URLSearchParams({
372
+ limit: String(HISTORY_LIMIT),
373
+ });
374
+
375
+ if (normalizedFilter.path) {
376
+ params.set('path', normalizedFilter.path);
377
+ }
378
+ if (normalizedFilter.method) {
379
+ params.set('method', normalizedFilter.method);
380
+ }
381
+
382
+ const result = await requestJson(`${API_BASE}/change-history?${params.toString()}`);
383
+ state.historyEvents = result.events;
384
+ state.historyFilter = result.filter;
385
+ renderChangeHistory();
386
+ }
387
+
388
+ async function compareSnapshots() {
389
+ const baselineSnapshotId = elements.baselineSelect.value;
390
+ const targetSnapshotId = elements.targetSelect.value;
391
+
392
+ if (!baselineSnapshotId || !targetSnapshotId) {
393
+ setAlert('Choose baseline and target snapshot first.', 'error');
394
+ return;
395
+ }
396
+
397
+ if (baselineSnapshotId === targetSnapshotId) {
398
+ setAlert('Baseline and target must be different.', 'error');
399
+ return;
400
+ }
401
+
402
+ const diffResult = await requestJson(
403
+ `${API_BASE}/diff?baselineSnapshotId=${encodeURIComponent(baselineSnapshotId)}&targetSnapshotId=${encodeURIComponent(
404
+ targetSnapshotId
405
+ )}`
406
+ );
407
+
408
+ state.diffResult = diffResult;
409
+ renderDiffSummary();
410
+ }
411
+
412
+ async function loadEndpointDetail(endpointPath, endpointMethod) {
413
+ const baselineSnapshotId = elements.baselineSelect.value;
414
+ const targetSnapshotId = elements.targetSelect.value;
415
+ const detail = await requestJson(
416
+ `${API_BASE}/endpoint-detail?baselineSnapshotId=${encodeURIComponent(
417
+ baselineSnapshotId
418
+ )}&targetSnapshotId=${encodeURIComponent(targetSnapshotId)}&path=${encodeURIComponent(
419
+ endpointPath
420
+ )}&method=${encodeURIComponent(endpointMethod)}`
421
+ );
422
+ await loadChangeHistory({
423
+ path: endpointPath,
424
+ method: endpointMethod,
425
+ });
426
+ renderEndpointDetail(detail);
427
+ }
428
+
429
+ async function captureSnapshot() {
430
+ const capturedDiff = await requestJson(`${API_BASE}/capture`, { method: 'POST' });
431
+ state.diffResult = capturedDiff;
432
+ await fetchSnapshots();
433
+ await loadChangeHistory({
434
+ path: state.historyFilter.path ?? undefined,
435
+ method: state.historyFilter.method ?? undefined,
436
+ });
437
+ if (capturedDiff.summary?.baselineSnapshotId) {
438
+ elements.baselineSelect.value = capturedDiff.summary.baselineSnapshotId;
439
+ }
440
+ if (capturedDiff.summary?.targetSnapshotId) {
441
+ elements.targetSelect.value = capturedDiff.summary.targetSnapshotId;
442
+ }
443
+ renderDiffSummary();
444
+ }
445
+
446
+ function setLoading(isLoading) {
447
+ elements.refreshButton.disabled = isLoading;
448
+ elements.captureButton.disabled = isLoading;
449
+ }
450
+
451
+ async function loadInitialData() {
452
+ setLoading(true);
453
+ try {
454
+ await fetchSnapshots();
455
+ await loadChangeHistory();
456
+ if (state.snapshots.length >= 2) {
457
+ await compareSnapshots();
458
+ } else {
459
+ renderDiffSummary();
460
+ }
461
+ setAlert('Dashboard loaded.');
462
+ } catch (error) {
463
+ const message = error instanceof Error ? error.message : String(error);
464
+ setAlert(message, 'error');
465
+ renderDiffSummary();
466
+ } finally {
467
+ setLoading(false);
468
+ }
469
+ }
470
+
471
+ elements.refreshButton.addEventListener('click', () => {
472
+ void loadInitialData();
473
+ });
474
+
475
+ elements.captureButton.addEventListener('click', () => {
476
+ setLoading(true);
477
+ void captureSnapshot()
478
+ .then(() => {
479
+ setAlert('Snapshot captured successfully.');
480
+ })
481
+ .catch(error => {
482
+ const message = error instanceof Error ? error.message : String(error);
483
+ setAlert(message, 'error');
484
+ })
485
+ .finally(() => {
486
+ setLoading(false);
487
+ });
488
+ });
489
+
490
+ elements.compareForm.addEventListener('submit', event => {
491
+ event.preventDefault();
492
+ setLoading(true);
493
+ void compareSnapshots()
494
+ .then(() => {
495
+ setAlert('Diff updated.');
496
+ resetDetail();
497
+ })
498
+ .catch(error => {
499
+ const message = error instanceof Error ? error.message : String(error);
500
+ setAlert(message, 'error');
501
+ })
502
+ .finally(() => {
503
+ setLoading(false);
504
+ });
505
+ });
506
+
507
+ elements.historyFilterForm.addEventListener('submit', event => {
508
+ event.preventDefault();
509
+ setLoading(true);
510
+ void loadChangeHistory(readHistoryFilterInputs())
511
+ .then(() => {
512
+ setAlert('Change history updated.');
513
+ })
514
+ .catch(error => {
515
+ const message = error instanceof Error ? error.message : String(error);
516
+ setAlert(message, 'error');
517
+ })
518
+ .finally(() => {
519
+ setLoading(false);
520
+ });
521
+ });
522
+
523
+ elements.historyClearButton.addEventListener('click', () => {
524
+ elements.historyPathInput.value = '';
525
+ elements.historyMethodSelect.value = '';
526
+ setLoading(true);
527
+ void loadChangeHistory()
528
+ .then(() => {
529
+ setAlert('Change history filter cleared.');
530
+ })
531
+ .catch(error => {
532
+ const message = error instanceof Error ? error.message : String(error);
533
+ setAlert(message, 'error');
534
+ })
535
+ .finally(() => {
536
+ setLoading(false);
537
+ });
538
+ });
539
+
540
+ void loadInitialData();
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "swagger-parser-mcp-server",
3
- "version": "2.0.4",
3
+ "version": "2.3.0",
4
4
  "description": "MCP server for parsing Swagger/OpenAPI JSON files",
5
+ "packageManager": "pnpm@9.0.0",
5
6
  "main": "dist/index.js",
6
7
  "type": "module",
7
8
  "bin": {
@@ -15,6 +16,27 @@
15
16
  "README.md",
16
17
  "package.json"
17
18
  ],
19
+ "scripts": {
20
+ "build": "tsc && node scripts/copy-postgres-migrations.mjs && node scripts/copy-web-assets.mjs",
21
+ "dev": "tsx src/index.ts",
22
+ "dev:http": "tsx src/http.ts",
23
+ "migrate": "tsx src/infrastructure/postgres/runMigrations.ts",
24
+ "start": "node dist/index.js",
25
+ "start:http": "node dist/http.js",
26
+ "test": "jest",
27
+ "test:db:mem": "jest --runInBand --cache=false src/tests/postgresMigrationSmoke.test.ts src/tests/snapshotCapturePipeline.test.ts",
28
+ "test:db:real": "node -e \"if(!process.env.TEST_DATABASE_URL){console.error('TEST_DATABASE_URL is required for test:db:real');process.exit(1)}\" && jest --runInBand --cache=false src/tests/snapshotPersistence.real.test.ts",
29
+ "test:watch": "jest --watch",
30
+ "test:coverage": "jest --coverage",
31
+ "lint": "eslint .",
32
+ "lint:fix": "eslint . --fix",
33
+ "format": "prettier --write .",
34
+ "format:check": "prettier --check .",
35
+ "typecheck": "tsc --noEmit",
36
+ "check-all": "pnpm run typecheck && pnpm run lint && pnpm run format:check",
37
+ "prepublishOnly": "pnpm run check-all && pnpm run build",
38
+ "preinstall": "npx only-allow pnpm"
39
+ },
18
40
  "keywords": [
19
41
  "mcp",
20
42
  "swagger",
@@ -34,6 +56,7 @@
34
56
  "@modelcontextprotocol/sdk": "^1.17.4",
35
57
  "axios": "^1.6.0",
36
58
  "openapi-types": "^12.1.3",
59
+ "pg": "^8.20.0",
37
60
  "swagger-schema-official": "2.0.0-bab6bed",
38
61
  "zod": "^3.22.0",
39
62
  "zod-to-json-schema": "^3.24.6"
@@ -45,32 +68,19 @@
45
68
  "@semantic-release/gitlab": "^13.2.8",
46
69
  "@types/jest": "^29.5.0",
47
70
  "@types/node": "^20.0.0",
71
+ "@types/pg": "^8.18.0",
48
72
  "@types/swagger-schema-official": "^2.0.25",
49
73
  "eslint": "^9.32.0",
50
74
  "eslint-config-prettier": "^10.1.8",
51
75
  "eslint-plugin-prettier": "^5.5.3",
52
76
  "globals": "^16.3.0",
53
77
  "jest": "^29.7.0",
78
+ "pg-mem": "^3.0.14",
54
79
  "prettier": "^3.6.2",
55
80
  "semantic-release": "^24.2.7",
56
81
  "ts-jest": "^29.1.0",
57
82
  "tsx": "^4.7.0",
58
83
  "typescript": "^5.3.0",
59
84
  "typescript-eslint": "^8.38.0"
60
- },
61
- "scripts": {
62
- "build": "tsc",
63
- "dev": "tsx src/index.ts",
64
- "start": "node dist/index.js",
65
- "test": "jest",
66
- "test:watch": "jest --watch",
67
- "test:coverage": "jest --coverage",
68
- "lint": "eslint .",
69
- "lint:fix": "eslint . --fix",
70
- "format": "prettier --write .",
71
- "format:check": "prettier --check .",
72
- "typecheck": "tsc --noEmit",
73
- "check-all": "pnpm run typecheck && pnpm run lint && pnpm run format:check",
74
- "preinstall": "npx only-allow pnpm"
75
85
  }
76
- }
86
+ }