@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.
- package/AGENTS.md +74 -0
- package/README.md +27 -0
- package/backend/actions/ChatMessage/executeScript.js +15 -5
- package/backend/actions/ChatThread/createChatMessage.js +7 -2
- package/backend/actions/ChatThread/getChatThread.js +8 -2
- package/backend/actions/ChatThread/listChatThreads.js +6 -1
- package/backend/actions/ChatThread/shareChatThread.js +8 -2
- package/backend/actions/ChatThread/streamChatMessage.js +7 -2
- package/backend/actions/ChatThread/toggleAgentMode.js +8 -2
- package/backend/actions/Dashboard/createDashboard.js +3 -0
- package/backend/actions/Dashboard/deleteDashboard.js +13 -3
- package/backend/actions/Dashboard/getDashboard.js +33 -10
- package/backend/actions/Dashboard/getDashboards.js +9 -1
- package/backend/actions/Dashboard/updateDashboard.js +9 -2
- package/backend/actions/Model/addField.js +9 -2
- package/backend/actions/Model/analyzeSchema.js +16 -4
- package/backend/actions/Model/createChatMessage.js +3 -0
- package/backend/actions/Model/createDocument.js +3 -0
- package/backend/actions/Model/deleteDocument.js +9 -3
- package/backend/actions/Model/deleteDocuments.js +9 -1
- package/backend/actions/Model/dropCollection.js +3 -0
- package/backend/actions/Model/dropIndex.js +3 -0
- package/backend/actions/Model/executeDocumentScript.js +33 -26
- package/backend/actions/Model/exportQueryResults.js +13 -1
- package/backend/actions/Model/getCollectionInfo.js +15 -3
- package/backend/actions/Model/getDocument.js +13 -2
- package/backend/actions/Model/getDocuments.js +25 -5
- package/backend/actions/Model/getDocumentsStream.js +38 -18
- package/backend/actions/Model/getEstimatedDocumentCounts.js +15 -2
- package/backend/actions/Model/getIndexes.js +3 -0
- package/backend/actions/Model/getSuggestedProjection.js +3 -0
- package/backend/actions/Model/listModels.js +3 -0
- package/backend/actions/Model/streamChatMessage.js +3 -0
- package/backend/actions/Model/streamDocumentChanges.js +3 -0
- package/backend/actions/Model/updateDocument.js +10 -2
- package/backend/actions/Model/updateDocuments.js +10 -2
- package/backend/actions/Model/validateDocument.js +15 -2
- package/backend/actions/Script/createScript.js +36 -0
- package/backend/actions/Script/executeScript.js +89 -0
- package/backend/actions/Script/getScript.js +43 -0
- package/backend/actions/Script/index.js +5 -0
- package/backend/actions/Task/cancelTask.js +9 -3
- package/backend/actions/Task/getTaskOverview.js +7 -3
- package/backend/actions/Task/getTasks.js +7 -3
- package/backend/actions/Task/getTasksOverTime.js +7 -3
- package/backend/actions/Task/rescheduleTask.js +9 -3
- package/backend/actions/Task/runTask.js +9 -3
- package/backend/actions/index.js +1 -0
- package/backend/authorize.js +6 -1
- package/backend/db/scriptSchema.js +29 -0
- package/backend/helpers/omitNullish.js +5 -0
- package/backend/helpers/operationOptions.js +26 -0
- package/backend/helpers/readOperationOptions.js +31 -0
- package/backend/index.js +6 -4
- package/backend/integrations/mothership.js +28 -0
- package/backend/mcp.js +140 -0
- package/backend/netlify.js +5 -3
- package/backend/next.js +5 -3
- package/backend/sandbox/createSandbox.js +2 -2
- package/backend/sandbox/createScriptDb.js +60 -20
- package/backend/util/archetypeToZodSchema.js +74 -0
- package/docs/mcp-oauth.md +106 -0
- package/eslint.config.js +1 -0
- package/eval/agent/run.js +1 -1
- package/express.js +183 -41
- package/frontend/index.js +3 -0
- package/frontend/public/app.js +740 -124
- package/frontend/public/tw.css +76 -0
- package/frontend/src/api.js +27 -1
- package/frontend/src/index.js +9 -1
- package/frontend/src/mcp/mcp.html +55 -0
- package/frontend/src/mcp/mcp.js +27 -0
- package/frontend/src/models/models.html +29 -8
- package/frontend/src/models/models.js +30 -2
- package/frontend/src/navbar/navbar.html +8 -0
- package/frontend/src/navbar/navbar.js +3 -0
- package/frontend/src/routes.js +16 -0
- package/frontend/src/script/script.html +105 -0
- package/frontend/src/script/script.js +98 -0
- package/frontend/src/tooltip/tooltip.html +28 -0
- package/frontend/src/tooltip/tooltip.js +94 -0
- package/index.d.ts +10 -1
- package/local.js +2 -1
- package/nest.d.ts +4 -0
- package/package.json +5 -3
- package/vercel.d.ts +4 -0
package/frontend/public/tw.css
CHANGED
|
@@ -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
|
}
|
package/frontend/src/api.js
CHANGED
|
@@ -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
|
-
|
|
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);
|
package/frontend/src/index.js
CHANGED
|
@@ -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
|
-
<
|
|
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
|
|
457
|
-
|
|
458
|
-
|
|
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
|
|
487
|
-
|
|
488
|
-
|
|
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
|
|
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
|
},
|
package/frontend/src/routes.js
CHANGED
|
@@ -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>
|