@mongoosejs/studio 0.5.13 → 0.6.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 (86) hide show
  1. package/AGENTS.md +74 -0
  2. package/README.md +27 -0
  3. package/backend/actions/ChatMessage/executeScript.js +15 -5
  4. package/backend/actions/ChatThread/createChatMessage.js +7 -2
  5. package/backend/actions/ChatThread/getChatThread.js +8 -2
  6. package/backend/actions/ChatThread/listChatThreads.js +6 -1
  7. package/backend/actions/ChatThread/shareChatThread.js +8 -2
  8. package/backend/actions/ChatThread/streamChatMessage.js +7 -2
  9. package/backend/actions/ChatThread/toggleAgentMode.js +8 -2
  10. package/backend/actions/Dashboard/createDashboard.js +3 -0
  11. package/backend/actions/Dashboard/deleteDashboard.js +13 -3
  12. package/backend/actions/Dashboard/getDashboard.js +33 -10
  13. package/backend/actions/Dashboard/getDashboards.js +9 -1
  14. package/backend/actions/Dashboard/updateDashboard.js +9 -2
  15. package/backend/actions/Model/addField.js +9 -2
  16. package/backend/actions/Model/analyzeSchema.js +16 -4
  17. package/backend/actions/Model/createChatMessage.js +3 -0
  18. package/backend/actions/Model/createDocument.js +3 -0
  19. package/backend/actions/Model/deleteDocument.js +9 -3
  20. package/backend/actions/Model/deleteDocuments.js +9 -1
  21. package/backend/actions/Model/dropCollection.js +3 -0
  22. package/backend/actions/Model/dropIndex.js +3 -0
  23. package/backend/actions/Model/executeDocumentScript.js +33 -26
  24. package/backend/actions/Model/exportQueryResults.js +13 -1
  25. package/backend/actions/Model/getCollectionInfo.js +15 -3
  26. package/backend/actions/Model/getDocument.js +13 -2
  27. package/backend/actions/Model/getDocuments.js +25 -5
  28. package/backend/actions/Model/getDocumentsStream.js +38 -18
  29. package/backend/actions/Model/getEstimatedDocumentCounts.js +15 -2
  30. package/backend/actions/Model/getIndexes.js +3 -0
  31. package/backend/actions/Model/getSuggestedProjection.js +3 -0
  32. package/backend/actions/Model/listModels.js +3 -0
  33. package/backend/actions/Model/streamChatMessage.js +3 -0
  34. package/backend/actions/Model/streamDocumentChanges.js +3 -0
  35. package/backend/actions/Model/updateDocument.js +10 -2
  36. package/backend/actions/Model/updateDocuments.js +10 -2
  37. package/backend/actions/Model/validateDocument.js +15 -2
  38. package/backend/actions/Script/createScript.js +36 -0
  39. package/backend/actions/Script/executeScript.js +89 -0
  40. package/backend/actions/Script/getScript.js +43 -0
  41. package/backend/actions/Script/index.js +5 -0
  42. package/backend/actions/Task/cancelTask.js +9 -3
  43. package/backend/actions/Task/getTaskOverview.js +7 -3
  44. package/backend/actions/Task/getTasks.js +7 -3
  45. package/backend/actions/Task/getTasksOverTime.js +7 -3
  46. package/backend/actions/Task/rescheduleTask.js +9 -3
  47. package/backend/actions/Task/runTask.js +9 -3
  48. package/backend/actions/index.js +1 -0
  49. package/backend/authorize.js +6 -1
  50. package/backend/db/scriptSchema.js +29 -0
  51. package/backend/helpers/omitNullish.js +5 -0
  52. package/backend/helpers/operationOptions.js +26 -0
  53. package/backend/helpers/readOperationOptions.js +31 -0
  54. package/backend/index.js +6 -4
  55. package/backend/integrations/mothership.js +28 -0
  56. package/backend/mcp.js +140 -0
  57. package/backend/netlify.js +5 -3
  58. package/backend/next.js +5 -3
  59. package/backend/sandbox/createSandbox.js +2 -2
  60. package/backend/sandbox/createScriptDb.js +60 -20
  61. package/backend/util/archetypeToZodSchema.js +74 -0
  62. package/docs/mcp-oauth.md +106 -0
  63. package/eslint.config.js +1 -0
  64. package/eval/agent/run.js +1 -1
  65. package/express.js +183 -41
  66. package/frontend/index.js +3 -0
  67. package/frontend/public/app.js +740 -124
  68. package/frontend/public/tw.css +76 -0
  69. package/frontend/src/api.js +27 -1
  70. package/frontend/src/index.js +9 -1
  71. package/frontend/src/mcp/mcp.html +55 -0
  72. package/frontend/src/mcp/mcp.js +27 -0
  73. package/frontend/src/models/models.html +29 -8
  74. package/frontend/src/models/models.js +30 -2
  75. package/frontend/src/navbar/navbar.html +8 -0
  76. package/frontend/src/navbar/navbar.js +3 -0
  77. package/frontend/src/routes.js +16 -0
  78. package/frontend/src/script/script.html +105 -0
  79. package/frontend/src/script/script.js +98 -0
  80. package/frontend/src/tooltip/tooltip.html +28 -0
  81. package/frontend/src/tooltip/tooltip.js +94 -0
  82. package/index.d.ts +10 -1
  83. package/local.js +2 -1
  84. package/nest.d.ts +4 -0
  85. package/package.json +5 -3
  86. package/vercel.d.ts +4 -0
@@ -664,6 +664,10 @@ video {
664
664
  bottom: 0px;
665
665
  }
666
666
 
667
+ .-bottom-1 {
668
+ bottom: -0.25rem;
669
+ }
670
+
667
671
  .-left-1 {
668
672
  left: -0.25rem;
669
673
  }
@@ -672,6 +676,10 @@ video {
672
676
  left: -0.75rem;
673
677
  }
674
678
 
679
+ .-top-1 {
680
+ top: -0.25rem;
681
+ }
682
+
675
683
  .bottom-0 {
676
684
  bottom: 0px;
677
685
  }
@@ -792,6 +800,10 @@ video {
792
800
  z-index: 10001;
793
801
  }
794
802
 
803
+ .z-\[1000\] {
804
+ z-index: 1000;
805
+ }
806
+
795
807
  .z-\[100\] {
796
808
  z-index: 100;
797
809
  }
@@ -1085,6 +1097,10 @@ video {
1085
1097
  height: 4rem;
1086
1098
  }
1087
1099
 
1100
+ .h-2 {
1101
+ height: 0.5rem;
1102
+ }
1103
+
1088
1104
  .h-20 {
1089
1105
  height: 5rem;
1090
1106
  }
@@ -1157,6 +1173,10 @@ video {
1157
1173
  height: 45vh;
1158
1174
  }
1159
1175
 
1176
+ .h-\[55vh\] {
1177
+ height: 55vh;
1178
+ }
1179
+
1160
1180
  .h-full {
1161
1181
  height: 100%;
1162
1182
  }
@@ -1189,6 +1209,10 @@ video {
1189
1209
  max-height: 55vh;
1190
1210
  }
1191
1211
 
1212
+ .max-h-\[65vh\] {
1213
+ max-height: 65vh;
1214
+ }
1215
+
1192
1216
  .min-h-0 {
1193
1217
  min-height: 0px;
1194
1218
  }
@@ -1265,6 +1289,10 @@ video {
1265
1289
  width: 4rem;
1266
1290
  }
1267
1291
 
1292
+ .w-2 {
1293
+ width: 0.5rem;
1294
+ }
1295
+
1268
1296
  .w-20 {
1269
1297
  width: 5rem;
1270
1298
  }
@@ -1365,6 +1393,11 @@ video {
1365
1393
  width: 100%;
1366
1394
  }
1367
1395
 
1396
+ .w-max {
1397
+ width: -moz-max-content;
1398
+ width: max-content;
1399
+ }
1400
+
1368
1401
  .min-w-0 {
1369
1402
  min-width: 0px;
1370
1403
  }
@@ -1441,6 +1474,10 @@ video {
1441
1474
  max-width: calc(100vw - 4rem);
1442
1475
  }
1443
1476
 
1477
+ .max-w-\[min\(20rem\2c calc\(100vw-1rem\)\)\] {
1478
+ max-width: min(20rem,calc(100vw - 1rem));
1479
+ }
1480
+
1444
1481
  .max-w-full {
1445
1482
  max-width: 100%;
1446
1483
  }
@@ -1555,6 +1592,11 @@ video {
1555
1592
  transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1556
1593
  }
1557
1594
 
1595
+ .rotate-45 {
1596
+ --tw-rotate: 45deg;
1597
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1598
+ }
1599
+
1558
1600
  .rotate-90 {
1559
1601
  --tw-rotate: 90deg;
1560
1602
  transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
@@ -1608,6 +1650,10 @@ video {
1608
1650
  resize: vertical;
1609
1651
  }
1610
1652
 
1653
+ .resize {
1654
+ resize: both;
1655
+ }
1656
+
1611
1657
  .snap-y {
1612
1658
  scroll-snap-type: y var(--tw-scroll-snap-strictness);
1613
1659
  }
@@ -1861,6 +1907,10 @@ video {
1861
1907
  text-overflow: ellipsis;
1862
1908
  }
1863
1909
 
1910
+ .whitespace-normal {
1911
+ white-space: normal;
1912
+ }
1913
+
1864
1914
  .whitespace-nowrap {
1865
1915
  white-space: nowrap;
1866
1916
  }
@@ -2184,6 +2234,11 @@ video {
2184
2234
  background-color: rgb(55 65 81 / var(--tw-bg-opacity));
2185
2235
  }
2186
2236
 
2237
+ .bg-gray-900 {
2238
+ --tw-bg-opacity: 1;
2239
+ background-color: rgb(17 24 39 / var(--tw-bg-opacity));
2240
+ }
2241
+
2187
2242
  .bg-green-100 {
2188
2243
  --tw-bg-opacity: 1;
2189
2244
  background-color: rgb(220 252 231 / var(--tw-bg-opacity));
@@ -2747,6 +2802,10 @@ video {
2747
2802
  font-weight: 500;
2748
2803
  }
2749
2804
 
2805
+ .font-normal {
2806
+ font-weight: 400;
2807
+ }
2808
+
2750
2809
  .font-semibold {
2751
2810
  font-weight: 600;
2752
2811
  }
@@ -2763,6 +2822,10 @@ video {
2763
2822
  font-style: italic;
2764
2823
  }
2765
2824
 
2825
+ .leading-4 {
2826
+ line-height: 1rem;
2827
+ }
2828
+
2766
2829
  .leading-6 {
2767
2830
  line-height: 1.5rem;
2768
2831
  }
@@ -3216,6 +3279,11 @@ video {
3216
3279
  --tw-ring-color: rgb(176 32 43 / var(--tw-ring-opacity));
3217
3280
  }
3218
3281
 
3282
+ .ring-red-200 {
3283
+ --tw-ring-opacity: 1;
3284
+ --tw-ring-color: rgb(254 202 202 / var(--tw-ring-opacity));
3285
+ }
3286
+
3219
3287
  .ring-red-400 {
3220
3288
  --tw-ring-opacity: 1;
3221
3289
  --tw-ring-color: rgb(248 113 113 / var(--tw-ring-opacity));
@@ -3837,6 +3905,10 @@ video {
3837
3905
  --tw-ring-offset-width: 2px;
3838
3906
  }
3839
3907
 
3908
+ .focus-visible\:rounded-md:focus-visible {
3909
+ border-radius: 0.375rem;
3910
+ }
3911
+
3840
3912
  .focus-visible\:opacity-100:focus-visible {
3841
3913
  opacity: 1;
3842
3914
  }
@@ -3853,6 +3925,10 @@ video {
3853
3925
  outline-offset: 2px;
3854
3926
  }
3855
3927
 
3928
+ .focus-visible\:outline-blue-600:focus-visible {
3929
+ outline-color: #2563eb;
3930
+ }
3931
+
3856
3932
  .focus-visible\:outline-edge-strong:focus-visible {
3857
3933
  outline-color: var(--color-edge-strong);
3858
3934
  }
@@ -164,6 +164,17 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
164
164
  return client.post('', { action: 'ChatMessage.executeScript', ...params }).then(res => res.data);
165
165
  }
166
166
  };
167
+ exports.Script = {
168
+ createScript(params) {
169
+ return client.post('', { action: 'Script.createScript', ...params }).then(res => res.data);
170
+ },
171
+ executeScript(params) {
172
+ return client.post('', { action: 'Script.executeScript', ...params }).then(res => res.data);
173
+ },
174
+ getScript(params) {
175
+ return client.post('', { action: 'Script.getScript', ...params }).then(res => res.data);
176
+ }
177
+ };
167
178
  exports.Model = {
168
179
  addField(params) {
169
180
  return client.post('', { action: 'Model.addField', ...params }).then(res => res.data);
@@ -229,7 +240,11 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
229
240
  getDocumentsStream: async function* getDocumentsStream(params) {
230
241
  const data = await client.post('', { action: 'Model.getDocuments', ...params }).then(res => res.data);
231
242
  yield { schemaPaths: data.schemaPaths, suggestedFields: data.suggestedFields };
232
- yield { numDocs: data.numDocs };
243
+ if (data.numDocsError) {
244
+ yield { numDocsError: data.numDocsError };
245
+ } else {
246
+ yield { numDocs: data.numDocs };
247
+ }
233
248
  for (const doc of data.docs) {
234
249
  yield { document: doc };
235
250
  }
@@ -359,6 +374,17 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
359
374
  return client.post('/ChatMessage/executeScript', params).then(res => res.data);
360
375
  }
361
376
  };
377
+ exports.Script = {
378
+ createScript: function createScript(params) {
379
+ return client.post('/Script/createScript', params).then(res => res.data);
380
+ },
381
+ executeScript: function executeScript(params) {
382
+ return client.post('/Script/executeScript', params).then(res => res.data);
383
+ },
384
+ getScript: function getScript(params) {
385
+ return client.post('/Script/getScript', params).then(res => res.data);
386
+ }
387
+ };
362
388
  exports.Model = {
363
389
  addField(params) {
364
390
  return client.post('/Model/addField', params).then(res => res.data);
@@ -20,7 +20,7 @@ const {
20
20
  } = require('./_util/recent-pages-history');
21
21
  appendCSS(require('vue-toastification/dist/index.css'));
22
22
 
23
- const TRACKED_RECENT_PAGE_ROUTE_NAMES = new Set(['model', 'document', 'dashboard', 'chat']);
23
+ const TRACKED_RECENT_PAGE_ROUTE_NAMES = new Set(['model', 'document', 'dashboard', 'chat', 'script']);
24
24
  const CHAT_ROUTE_NAMES = new Set(['chat index', 'chat']);
25
25
 
26
26
  function formatHistoryLabel(route) {
@@ -42,6 +42,9 @@ function formatHistoryLabel(route) {
42
42
  if (route.name === 'chat') {
43
43
  return route.params?.threadId ? `Chat: ${route.params.threadId}` : 'Chat';
44
44
  }
45
+ if (route.name === 'script') {
46
+ return route.params?.scriptId ? `Script: ${route.params.scriptId}` : 'Script';
47
+ }
45
48
  const normalizedPath = route.path.replace(/^\//, '');
46
49
  return normalizedPath || 'Home';
47
50
  }
@@ -332,6 +335,11 @@ const router = VueRouter.createRouter({
332
335
 
333
336
  // Add global navigation guard
334
337
  router.beforeEach((to, from, next) => {
338
+ if (to.name === 'mcp' && window.MONGOOSE_STUDIO_CONFIG.mcp === false) {
339
+ next({ name: window.state?.roles?.[0] === 'dashboards' ? 'dashboards' : 'root' });
340
+ return;
341
+ }
342
+
335
343
  // Skip auth check for authorized (public) routes
336
344
  if (to.meta.authorized) {
337
345
  next();
@@ -0,0 +1,55 @@
1
+ <div class="mx-auto max-w-5xl py-6 px-2">
2
+ <h1 class="text-xl font-bold text-content">MCP</h1>
3
+ <p class="mt-2 text-sm text-content-secondary">
4
+ Connect an MCP client such as Claude or ChatGPT to this Mongoose Studio workspace.
5
+ </p>
6
+
7
+ <div class="mt-6 rounded-lg border border-edge bg-surface p-4">
8
+ <label for="mcp-url" class="block text-sm font-medium text-content">MCP URL</label>
9
+ <div class="mt-2 flex gap-2">
10
+ <input
11
+ id="mcp-url"
12
+ type="text"
13
+ readonly
14
+ :value="mcpUrl"
15
+ class="min-w-0 flex-1 rounded-md bg-muted px-3 py-2 font-mono text-sm text-content outline outline-1 -outline-offset-1 outline-edge-strong"
16
+ />
17
+ <async-button
18
+ type="button"
19
+ @click="copyMCPUrl"
20
+ class="rounded-md bg-primary px-4 py-2 text-sm font-semibold text-primary-text shadow-sm hover:bg-primary-hover">
21
+ Copy
22
+ </async-button>
23
+ </div>
24
+ </div>
25
+
26
+ <div class="mt-8 grid gap-6 text-sm text-content-secondary">
27
+ <section>
28
+ <h2 class="text-base font-semibold text-content">Authentication and network access</h2>
29
+ <p class="mt-2">
30
+ Without a Mongoose Studio API key, the MCP endpoint uses the deployment's
31
+ <code class="rounded bg-muted px-1 py-0.5">bindIp</code> restrictions. By default, this means an MCP client can only connect from localhost.
32
+ </p>
33
+ <p class="mt-2">
34
+ With an API key, MCP clients such as Claude and ChatGPT connect through OAuth. You will be prompted to sign in to Mongoose Studio and approve the connection before the client receives access.
35
+ </p>
36
+ </section>
37
+
38
+ <section>
39
+ <h2 class="text-base font-semibold text-content">Permissions</h2>
40
+ <p class="mt-2">
41
+ When an API key is configured, MCP access is scoped to the signed-in user's workspace permissions. During authorization, the user can explicitly grant fewer permissions, such as read-only access or a lower execution time limit. An MCP connection can never receive more access than that user already has.
42
+ </p>
43
+ </section>
44
+
45
+ <section>
46
+ <h2 class="text-base font-semibold text-content">Supported operations</h2>
47
+ <p class="mt-2">
48
+ The MCP server supports database CRUD operations, dashboards, and script creation. It does not execute standalone scripts directly.
49
+ </p>
50
+ <p class="mt-2">
51
+ An MCP client can create a script and return a Mongoose Studio URL. A person can then open that URL, review or edit the code, and choose whether to dry run or execute it.
52
+ </p>
53
+ </section>
54
+ </div>
55
+ </div>
@@ -0,0 +1,27 @@
1
+ 'use strict';
2
+
3
+ const template = require('./mcp.html');
4
+
5
+ module.exports = {
6
+ template,
7
+ computed: {
8
+ mcpUrl() {
9
+ const configuredUrl = window.MONGOOSE_STUDIO_CONFIG.mcpUrl;
10
+ if (configuredUrl) {
11
+ return configuredUrl;
12
+ }
13
+
14
+ const url = new URL(window.location.href);
15
+ url.hash = '';
16
+ url.search = '';
17
+ url.pathname = `${url.pathname.replace(/\/+$/, '')}/mcp`;
18
+ return url.toString();
19
+ }
20
+ },
21
+ methods: {
22
+ async copyMCPUrl() {
23
+ await navigator.clipboard.writeText(this.mcpUrl);
24
+ this.$toast.success('MCP URL copied!');
25
+ }
26
+ }
27
+ };
@@ -110,7 +110,18 @@
110
110
  >
111
111
  </document-search>
112
112
  <div>
113
- <span v-if="numDocuments == null">Loading ...</span>
113
+ <tooltip
114
+ v-if="numDocumentsError"
115
+ :text="'Error counting documents: ' + numDocumentsError"
116
+ >
117
+ <span
118
+ class="inline-flex rounded-md bg-red-50 px-2 py-2 text-sm text-red-700 ring-1 ring-inset ring-red-200"
119
+ role="alert"
120
+ >
121
+ Count Error
122
+ </span>
123
+ </tooltip>
124
+ <span v-else-if="numDocuments == null">Loading...</span>
114
125
  <span v-else-if="typeof numDocuments === 'number'">{{documents.length}}/{{numDocuments === 1 ? numDocuments + ' document' : numDocuments + ' documents'}}</span>
115
126
  </div>
116
127
  <button
@@ -453,11 +464,16 @@
453
464
  </tbody>
454
465
  </table>
455
466
  </div>
456
- <div v-if="outputType === 'table' && (loadingMore || (status === 'loading' && documents.length > 0))" class="flex items-center justify-center gap-2 py-3 text-sm text-content-tertiary border-t border-edge bg-surface">
457
- <img src="images/loader.gif" alt="" class="h-5 w-5">
458
- <span>Loading documents…</span>
467
+ <div
468
+ v-if="outputType === 'table' && (documentsError || loadingMore || (status === 'loading' && documents.length > 0))"
469
+ class="flex items-center justify-center gap-2 py-3 text-sm border-t"
470
+ :class="documentsError ? 'border-red-200 bg-red-50 text-red-700' : 'border-edge bg-surface text-content-tertiary'"
471
+ :role="documentsError ? 'alert' : null"
472
+ >
473
+ <img v-if="!documentsError" src="images/loader.gif" alt="" class="h-5 w-5">
474
+ <span>{{documentsError || 'Loading documents…'}}</span>
459
475
  </div>
460
- <p v-if="outputType === 'table' && documents.length === 0 && status === 'loaded'" class="mt-2 text-sm text-content-tertiary px-1">
476
+ <p v-if="outputType === 'table' && documents.length === 0 && status === 'loaded' && !documentsError" class="mt-2 text-sm text-content-tertiary px-1">
461
477
  No documents to show. Use Projection in the menu to choose columns.
462
478
  </p>
463
479
  </div>
@@ -483,9 +499,14 @@
483
499
  <list-json :value="filterDocument(document)" :references="referenceMap">
484
500
  </list-json>
485
501
  </div>
486
- <div v-if="outputType === 'json' && (loadingMore || (status === 'loading' && documents.length > 0))" class="flex items-center justify-center gap-2 py-3 text-sm text-content-tertiary">
487
- <img src="images/loader.gif" alt="" class="h-5 w-5">
488
- <span>Loading documents…</span>
502
+ <div
503
+ v-if="outputType === 'json' && (documentsError || loadingMore || (status === 'loading' && documents.length > 0))"
504
+ class="flex items-center justify-center gap-2 py-3 text-sm"
505
+ :class="documentsError ? 'rounded-md border border-red-200 bg-red-50 text-red-700' : 'text-content-tertiary'"
506
+ :role="documentsError ? 'alert' : null"
507
+ >
508
+ <img v-if="!documentsError" src="images/loader.gif" alt="" class="h-5 w-5">
509
+ <span>{{documentsError || 'Loading documents…'}}</span>
489
510
  </div>
490
511
  </div>
491
512
  <div v-else-if="outputType === 'map'" class="flex flex-col h-full">
@@ -151,6 +151,8 @@ module.exports = app => app.component('models', {
151
151
  filteredPaths: [],
152
152
  selectedPaths: [],
153
153
  numDocuments: null,
154
+ numDocumentsError: null,
155
+ documentsError: null,
154
156
  mongoDBIndexes: [],
155
157
  schemaIndexes: [],
156
158
  status: 'loading',
@@ -838,7 +840,8 @@ module.exports = app => app.component('models', {
838
840
  return filteredDoc;
839
841
  },
840
842
  async checkIfScrolledToBottom() {
841
- if (this.status === 'loading' || this.loadedAllDocs) {
843
+ // Stop infinite scroll if the last load failed, otherwise we would retry the same failing request forever.
844
+ if (this.status === 'loading' || this.loadedAllDocs || this.documentsError) {
842
845
  return;
843
846
  }
844
847
  // Infinite scroll only applies to table/json views.
@@ -864,11 +867,20 @@ module.exports = app => app.component('models', {
864
867
  try {
865
868
  const skip = this.documents.length;
866
869
  const params = this.buildDocumentFetchParams({ skip });
867
- const { docs } = await api.Model.getDocuments(params);
870
+ const { docs, numDocs, numDocsError } = await api.Model.getDocuments(params);
871
+ if (numDocsError) {
872
+ this.numDocumentsError = numDocsError;
873
+ } else if (numDocs !== undefined) {
874
+ this.numDocuments = numDocs;
875
+ this.numDocumentsError = null;
876
+ }
868
877
  if (docs.length < limit) {
869
878
  this.loadedAllDocs = true;
870
879
  }
871
880
  this.documents.push(...docs);
881
+ this.documentsError = null;
882
+ } catch (err) {
883
+ this.documentsError = err?.message;
872
884
  } finally {
873
885
  this.loadingMore = false;
874
886
  this.status = 'loaded';
@@ -1081,6 +1093,8 @@ module.exports = app => app.component('models', {
1081
1093
  // Clear previous data
1082
1094
  this.documents = [];
1083
1095
  this.numDocuments = null;
1096
+ this.numDocumentsError = null;
1097
+ this.documentsError = null;
1084
1098
  this.loadedAllDocs = false;
1085
1099
  this.lastSelectedIndex = null;
1086
1100
 
@@ -1130,6 +1144,10 @@ module.exports = app => app.component('models', {
1130
1144
  }
1131
1145
  if (event.numDocs !== undefined) {
1132
1146
  this.numDocuments = event.numDocs;
1147
+ this.numDocumentsError = null;
1148
+ }
1149
+ if (event.numDocsError) {
1150
+ this.numDocumentsError = event.numDocsError;
1133
1151
  }
1134
1152
  if (event.document) {
1135
1153
  this.documents.push(event.document);
@@ -1143,6 +1161,9 @@ module.exports = app => app.component('models', {
1143
1161
  if (docsCount < limit) {
1144
1162
  this.loadedAllDocs = true;
1145
1163
  }
1164
+ this.documentsError = null;
1165
+ } catch (err) {
1166
+ this.documentsError = err?.message;
1146
1167
  } finally {
1147
1168
  this.status = 'loaded';
1148
1169
  }
@@ -1169,8 +1190,12 @@ module.exports = app => app.component('models', {
1169
1190
  for await (const event of api.Model.getDocumentsStream(params)) {
1170
1191
  if (event.numDocs !== undefined && !numDocsReceived) {
1171
1192
  this.numDocuments = event.numDocs;
1193
+ this.numDocumentsError = null;
1172
1194
  numDocsReceived = true;
1173
1195
  }
1196
+ if (event.numDocsError) {
1197
+ this.numDocumentsError = event.numDocsError;
1198
+ }
1174
1199
  if (event.document) {
1175
1200
  this.documents.push(event.document);
1176
1201
  docsCount++;
@@ -1183,6 +1208,9 @@ module.exports = app => app.component('models', {
1183
1208
  if (docsCount < limit) {
1184
1209
  this.loadedAllDocs = true;
1185
1210
  }
1211
+ this.documentsError = null;
1212
+ } catch (err) {
1213
+ this.documentsError = err?.message;
1186
1214
  } finally {
1187
1215
  this.loadingMore = false;
1188
1216
  this.status = 'loaded';
@@ -147,6 +147,10 @@
147
147
  <path fill-rule="evenodd" d="M10 2a4 4 0 00-4 4v2H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-1V6a4 4 0 00-4-4zm-3 6V6a3 3 0 116 0v2H7z" clip-rule="evenodd" />
148
148
  </svg>
149
149
  </span>
150
+ <router-link v-if="mcpEnabled" to="/mcp" @click="showFlyout = false" class="cursor-pointer block px-4 py-2 text-sm text-content-secondary hover:bg-primary-subtle" role="menuitem" tabindex="-1">MCP</router-link>
151
+ <span v-else class="block px-4 py-2 text-sm text-gray-300 cursor-not-allowed" role="menuitem" title="MCP is disabled for this deployment">
152
+ MCP
153
+ </span>
150
154
  <span @click="logout" class="cursor-pointer block px-4 py-2 text-sm text-content-secondary hover:bg-primary-subtle" role="menuitem" tabindex="-1" id="user-menu-item-2">Sign out</span>
151
155
  </div>
152
156
  </div>
@@ -287,6 +291,10 @@
287
291
  <path fill-rule="evenodd" d="M10 2a4 4 0 00-4 4v2H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-1V6a4 4 0 00-4-4zm-3 6V6a3 3 0 116 0v2H7z" clip-rule="evenodd" />
288
292
  </svg>
289
293
  </span>
294
+ <router-link v-if="mcpEnabled" to="/mcp" class="block px-3 py-2 rounded-md text-base text-content-secondary hover:bg-primary-subtle">MCP</router-link>
295
+ <span v-else class="block px-3 py-2 rounded-md text-base text-gray-300 cursor-not-allowed" title="MCP is disabled for this deployment">
296
+ MCP
297
+ </span>
290
298
  <span @click="logout" class="block px-3 py-2 rounded-md text-base text-content-secondary hover:bg-primary-subtle cursor-pointer">Sign out</span>
291
299
  </div>
292
300
  </div>
@@ -103,6 +103,9 @@ module.exports = app => app.component('navbar', {
103
103
  hasTaskVisualizer() {
104
104
  return !!window.MONGOOSE_STUDIO_CONFIG.enableTaskVisualizer;
105
105
  },
106
+ mcpEnabled() {
107
+ return window.MONGOOSE_STUDIO_CONFIG.mcp !== false;
108
+ },
106
109
  recentPagesList() {
107
110
  return this.localRecentPages;
108
111
  },
@@ -69,6 +69,14 @@ module.exports = {
69
69
  authorized: false
70
70
  }
71
71
  },
72
+ {
73
+ path: '/mcp',
74
+ name: 'mcp',
75
+ component: 'mcp',
76
+ meta: {
77
+ authorized: true
78
+ }
79
+ },
72
80
  {
73
81
  path: '/tasks',
74
82
  name: 'tasks',
@@ -108,6 +116,14 @@ module.exports = {
108
116
  meta: {
109
117
  authorized: true
110
118
  }
119
+ },
120
+ {
121
+ path: '/script/:scriptId',
122
+ name: 'script',
123
+ component: 'script',
124
+ meta: {
125
+ authorized: true
126
+ }
111
127
  }
112
128
  ],
113
129
  roleAccess,
@@ -0,0 +1,105 @@
1
+ <div class="px-4 py-6">
2
+ <div v-if="status === 'loading'" class="max-w-5xl mx-auto text-center">
3
+ <img src="images/loader.gif" class="inline mt-10" alt="Loading script">
4
+ </div>
5
+
6
+ <div v-else-if="script" class="max-w-5xl mx-auto">
7
+ <div class="flex items-center justify-between mb-4">
8
+ <div>
9
+ <h1 class="text-xl font-semibold text-content">Script</h1>
10
+ <p class="text-xs text-content-tertiary font-mono">{{ script._id }}</p>
11
+ </div>
12
+ <div class="flex items-center">
13
+ <button
14
+ v-if="!isEditing"
15
+ class="px-3 py-1.5 mr-2 text-sm bg-gray-700 text-white rounded hover:bg-gray-600"
16
+ @click="startEditing">
17
+ Edit
18
+ </button>
19
+ <div class="relative flex items-center">
20
+ <async-button
21
+ class="px-3 py-1.5 text-sm bg-primary hover:bg-primary-hover text-primary-text rounded-l-md"
22
+ @click="executeScript">
23
+ {{ selectedRunLabel }}
24
+ </async-button>
25
+ <button
26
+ class="px-1.5 py-1.5 text-sm bg-primary hover:bg-primary-hover text-primary-text border-l border-black/10 rounded-r-md"
27
+ @click.stop="showRunDropdown = !showRunDropdown">
28
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
29
+ <path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd" />
30
+ </svg>
31
+ </button>
32
+ <div
33
+ v-if="showRunDropdown"
34
+ class="absolute right-0 top-full z-10 mt-1 w-40 rounded-md bg-surface py-1 shadow-lg ring-1 ring-black/5">
35
+ <button class="block w-full px-4 py-2 text-left text-xs text-content-secondary hover:bg-muted" @click="selectRunMode('run')">
36
+ Run
37
+ </button>
38
+ <button
39
+ class="block w-full px-4 py-2 text-left text-xs"
40
+ :class="canUseDryRun ? 'text-content-secondary hover:bg-muted' : 'cursor-not-allowed text-gray-400'"
41
+ :disabled="!canUseDryRun"
42
+ :title="canUseDryRun ? null : 'Dry run mode requires MongoDB transaction support'"
43
+ @click="selectRunMode('dryRun')">
44
+ Dry Run
45
+ </button>
46
+ </div>
47
+ </div>
48
+ </div>
49
+ </div>
50
+
51
+ <div class="relative border rounded bg-surface text-content text-sm overflow-hidden">
52
+ <div class="flex border-b py-1 pl-1 text-xs font-medium bg-surface">
53
+ <button
54
+ class="px-4 py-1 rounded-l-md border-r border-edge-strong font-semibold"
55
+ :class="activeTab === 'code' ? 'bg-gray-700 text-white shadow' : 'bg-muted text-gray-600'"
56
+ @click="activeTab = 'code'">
57
+ Code
58
+ </button>
59
+ <button
60
+ class="px-4 py-1 rounded-r-md font-semibold"
61
+ :class="activeTab === 'output' ? 'bg-gray-700 text-white shadow' : 'bg-muted text-gray-600'"
62
+ @click="activeTab = 'output'">
63
+ Output<span v-if="isDryRunResult" class="ml-1">[Dry Run]</span>
64
+ </button>
65
+ </div>
66
+
67
+ <div v-show="activeTab === 'code'" class="max-h-[65vh] overflow-y-auto">
68
+ <div v-if="isEditing" class="flex flex-col">
69
+ <ace-editor
70
+ v-model="editedScript"
71
+ mode="javascript"
72
+ :line-numbers="true"
73
+ class="w-full h-[55vh] min-h-[300px]"
74
+ />
75
+ <div class="flex justify-end p-2 border-t border-edge">
76
+ <button class="px-3 py-1.5 text-xs bg-gray-300 text-gray-800 rounded hover:bg-gray-400" @click="cancelEditing">
77
+ Cancel
78
+ </button>
79
+ </div>
80
+ </div>
81
+ <pre v-else class="whitespace-pre-wrap !my-0 bg-muted"><code ref="code" class="language-javascript" v-text="script.script"></code></pre>
82
+ </div>
83
+
84
+ <div v-show="activeTab === 'output'" class="p-3 max-h-[65vh] overflow-y-auto border-t">
85
+ <div v-if="script.executionResult?.error" class="rounded-md bg-red-50 p-4 mb-3 text-sm text-red-800">
86
+ {{ script.executionResult.error }}
87
+ </div>
88
+ <dashboard-result
89
+ v-if="script.executionResult?.output != null"
90
+ :result="script.executionResult.output">
91
+ </dashboard-result>
92
+ <pre v-else-if="!script.executionResult?.error">No output</pre>
93
+
94
+ <div v-if="script.executionResult?.logs" class="mt-3 pt-3 border-t border-edge">
95
+ <div class="text-xs font-semibold text-gray-600 uppercase tracking-wide">Console</div>
96
+ <pre class="mt-1 bg-muted text-content p-3 rounded whitespace-pre-wrap overflow-x-auto max-h-[280px]">{{ script.executionResult.logs }}</pre>
97
+ </div>
98
+ </div>
99
+ </div>
100
+ </div>
101
+
102
+ <div v-else class="max-w-5xl mx-auto text-content-secondary">
103
+ No script with the given id could be found.
104
+ </div>
105
+ </div>