@xen-orchestra/rest-api 0.31.1 → 0.32.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/dist/acl-privileges/acl-privilege.controller.mjs +7 -2
- package/dist/acl-roles/acl-role.controller.mjs +13 -2
- package/dist/alarms/alarm.controller.mjs +3 -1
- package/dist/backup-archives/backup-archive.controller.mjs +3 -1
- package/dist/backup-jobs/backup-job.controller.mjs +12 -1
- package/dist/backup-logs/backup-log.controller.mjs +3 -1
- package/dist/backup-repositories/backup-repositories.controller.mjs +3 -1
- package/dist/events/event.controller.mjs +4 -1
- package/dist/groups/group.controller.mjs +10 -1
- package/dist/hosts/host.controller.mjs +16 -1
- package/dist/index.mjs +2 -0
- package/dist/mcp/mcp.controller.mjs +59 -0
- package/dist/mcp/mcp.helper.mjs +11 -0
- package/dist/messages/message.controller.mjs +3 -1
- package/dist/middlewares/mcp-gate.middleware.mjs +30 -0
- package/dist/networks/network.controller.mjs +9 -1
- package/dist/open-api/routes/routes.js +33 -1
- package/dist/pbds/pbd.controller.mjs +5 -1
- package/dist/pcis/pci.controller.mjs +3 -1
- package/dist/pgpus/pgpu.controller.mjs +3 -1
- package/dist/pifs/pif.controller.mjs +6 -1
- package/dist/pools/pool.controller.mjs +20 -1
- package/dist/proxies/proxy.controller.mjs +3 -1
- package/dist/restore-logs/restore-log.controller.mjs +5 -1
- package/dist/schedules/schedule.controller.mjs +4 -1
- package/dist/servers/server.controller.mjs +8 -1
- package/dist/sms/sm.controller.mjs +3 -1
- package/dist/srs/sr.controller.mjs +13 -1
- package/dist/tasks/task.controller.mjs +6 -1
- package/dist/users/user.controller.mjs +13 -2
- package/dist/vbds/vbd.controller.mjs +10 -1
- package/dist/vdi-snapshots/vdi-snapshot.controller.mjs +10 -1
- package/dist/vdis/vdi.controller.mjs +13 -1
- package/dist/vifs/vif.controller.mjs +10 -1
- package/dist/vm-controller/vm-controller.controller.mjs +9 -1
- package/dist/vm-snapshots/vm-snapshot.controller.mjs +11 -1
- package/dist/vm-templates/vm-template.controller.mjs +11 -1
- package/dist/vms/vm.controller.mjs +29 -1
- package/dist/xoa/xoa.controller.mjs +4 -1
- package/eslint-rules/index.cjs +7 -0
- package/eslint-rules/require-mcp-expose.cjs +129 -0
- package/open-api/spec/swagger.json +563 -264
- package/package.json +2 -2
- package/tsoa.json +2 -1
|
@@ -7,7 +7,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
8
8
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
9
9
|
};
|
|
10
|
-
import { Delete, Example, Get, Path, Put, Query, Request, Response, Route, Security, SuccessResponse, Tags,
|
|
10
|
+
import { Delete, Example, Extension, Get, Middlewares, Path, Put, Query, Request, Response, Route, Security, SuccessResponse, Tags, } from 'tsoa';
|
|
11
11
|
import { inject } from 'inversify';
|
|
12
12
|
import { AlarmService } from '../alarms/alarm.service.mjs';
|
|
13
13
|
import { escapeUnsafeComplexMatcher, limitAndFilterArray } from '../helpers/utils.helper.mjs';
|
|
@@ -178,6 +178,7 @@ let VmSnapshotController = class VmSnapshotController extends XapiXoController {
|
|
|
178
178
|
__decorate([
|
|
179
179
|
Example(vmSnapshotIds),
|
|
180
180
|
Example(partialVmSnapshots),
|
|
181
|
+
Extension('x-mcp-exposure', 'allow'),
|
|
181
182
|
Get(''),
|
|
182
183
|
Security('*', ['acl']),
|
|
183
184
|
__param(0, Request()),
|
|
@@ -188,6 +189,7 @@ __decorate([
|
|
|
188
189
|
__param(5, Query())
|
|
189
190
|
], VmSnapshotController.prototype, "getVmSnapshots", null);
|
|
190
191
|
__decorate([
|
|
192
|
+
Extension('x-mcp-exposure', 'deny'),
|
|
191
193
|
Get('{id}.{format}'),
|
|
192
194
|
Middlewares(acl({ resource: 'vm-snapshot', action: 'export', objectId: 'params.id' })),
|
|
193
195
|
SuccessResponse(200, 'Download started', 'application/octet-stream'),
|
|
@@ -201,6 +203,7 @@ __decorate([
|
|
|
201
203
|
], VmSnapshotController.prototype, "exportVmSnapshot", null);
|
|
202
204
|
__decorate([
|
|
203
205
|
Example(vmSnapshot),
|
|
206
|
+
Extension('x-mcp-exposure', 'allow'),
|
|
204
207
|
Get('{id}'),
|
|
205
208
|
Middlewares(acl({ resource: 'vm-snapshot', action: 'read', objectId: 'params.id' })),
|
|
206
209
|
Response(forbiddenOperationResp.status, forbiddenOperationResp.description),
|
|
@@ -208,6 +211,7 @@ __decorate([
|
|
|
208
211
|
__param(0, Path())
|
|
209
212
|
], VmSnapshotController.prototype, "getVmSnapshot", null);
|
|
210
213
|
__decorate([
|
|
214
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
211
215
|
Delete('{id}'),
|
|
212
216
|
Middlewares(acl({ resource: 'vm-snapshot', action: 'delete', objectId: 'params.id' })),
|
|
213
217
|
SuccessResponse(noContentResp.status, noContentResp.description),
|
|
@@ -218,6 +222,7 @@ __decorate([
|
|
|
218
222
|
], VmSnapshotController.prototype, "deleteVmSnapshot", null);
|
|
219
223
|
__decorate([
|
|
220
224
|
Example(genericAlarmsExample),
|
|
225
|
+
Extension('x-mcp-exposure', 'allow'),
|
|
221
226
|
Get('{id}/alarms'),
|
|
222
227
|
Security('*', ['acl']),
|
|
223
228
|
Tags('alarms'),
|
|
@@ -232,6 +237,7 @@ __decorate([
|
|
|
232
237
|
], VmSnapshotController.prototype, "getVmSnapshotAlarms", null);
|
|
233
238
|
__decorate([
|
|
234
239
|
Example(vmSnapshotVdis),
|
|
240
|
+
Extension('x-mcp-exposure', 'allow'),
|
|
235
241
|
Get('{id}/vdis'),
|
|
236
242
|
Security('*', ['acl']),
|
|
237
243
|
Tags('vdis'),
|
|
@@ -247,6 +253,7 @@ __decorate([
|
|
|
247
253
|
__decorate([
|
|
248
254
|
Example(messageIds),
|
|
249
255
|
Example(partialMessages),
|
|
256
|
+
Extension('x-mcp-exposure', 'allow'),
|
|
250
257
|
Get('{id}/messages'),
|
|
251
258
|
Security('*', ['acl']),
|
|
252
259
|
Tags('messages'),
|
|
@@ -262,6 +269,7 @@ __decorate([
|
|
|
262
269
|
__decorate([
|
|
263
270
|
Example(taskIds),
|
|
264
271
|
Example(partialTasks),
|
|
272
|
+
Extension('x-mcp-exposure', 'allow'),
|
|
265
273
|
Get('{id}/tasks'),
|
|
266
274
|
Security('*', ['acl']),
|
|
267
275
|
Tags('tasks'),
|
|
@@ -275,6 +283,7 @@ __decorate([
|
|
|
275
283
|
__param(6, Query())
|
|
276
284
|
], VmSnapshotController.prototype, "getVmSnapshotTasks", null);
|
|
277
285
|
__decorate([
|
|
286
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
278
287
|
Put('{id}/tags/{tag}'),
|
|
279
288
|
Middlewares(acl({ resource: 'vm-snapshot', action: 'update:tags', objectId: 'params.id' })),
|
|
280
289
|
SuccessResponse(noContentResp.status, noContentResp.description),
|
|
@@ -284,6 +293,7 @@ __decorate([
|
|
|
284
293
|
__param(1, Path())
|
|
285
294
|
], VmSnapshotController.prototype, "putVmSnapshotTag", null);
|
|
286
295
|
__decorate([
|
|
296
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
287
297
|
Delete('{id}/tags/{tag}'),
|
|
288
298
|
Middlewares(acl({ resource: 'vm-snapshot', action: 'update:tags', objectId: 'params.id' })),
|
|
289
299
|
SuccessResponse(noContentResp.status, noContentResp.description),
|
|
@@ -7,7 +7,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
8
8
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
9
9
|
};
|
|
10
|
-
import { Example, Get,
|
|
10
|
+
import { Delete, Example, Extension, Get, Middlewares, Path, Put, Query, Request, Response, Route, Security, SuccessResponse, Tags, } from 'tsoa';
|
|
11
11
|
import { inject } from 'inversify';
|
|
12
12
|
import { provide } from 'inversify-binding-decorators';
|
|
13
13
|
import { AlarmService } from '../alarms/alarm.service.mjs';
|
|
@@ -178,6 +178,7 @@ let VmTemplateController = class VmTemplateController extends XapiXoController {
|
|
|
178
178
|
__decorate([
|
|
179
179
|
Example(vmTemplateIds),
|
|
180
180
|
Example(partialVmTemplates),
|
|
181
|
+
Extension('x-mcp-exposure', 'allow'),
|
|
181
182
|
Get(''),
|
|
182
183
|
Security('*', ['acl']),
|
|
183
184
|
__param(0, Request()),
|
|
@@ -188,6 +189,7 @@ __decorate([
|
|
|
188
189
|
__param(5, Query())
|
|
189
190
|
], VmTemplateController.prototype, "getVmTemplates", null);
|
|
190
191
|
__decorate([
|
|
192
|
+
Extension('x-mcp-exposure', 'deny'),
|
|
191
193
|
Get('{id}.{format}'),
|
|
192
194
|
Middlewares(acl({ resource: 'vm-template', action: 'export', objectId: 'params.id' })),
|
|
193
195
|
SuccessResponse(200, 'Download started', 'application/octet-stream'),
|
|
@@ -201,6 +203,7 @@ __decorate([
|
|
|
201
203
|
], VmTemplateController.prototype, "exportVmTemplate", null);
|
|
202
204
|
__decorate([
|
|
203
205
|
Example(vmTemplate),
|
|
206
|
+
Extension('x-mcp-exposure', 'allow'),
|
|
204
207
|
Get('{id}'),
|
|
205
208
|
Middlewares(acl({ resource: 'vm-template', action: 'read', objectId: 'params.id' })),
|
|
206
209
|
Response(forbiddenOperationResp.status, forbiddenOperationResp.description),
|
|
@@ -208,6 +211,7 @@ __decorate([
|
|
|
208
211
|
__param(0, Path())
|
|
209
212
|
], VmTemplateController.prototype, "getVmTemplate", null);
|
|
210
213
|
__decorate([
|
|
214
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
211
215
|
Delete('{id}'),
|
|
212
216
|
Middlewares(acl({ resource: 'vm-template', action: 'delete', objectId: 'params.id' })),
|
|
213
217
|
SuccessResponse(noContentResp.status, noContentResp.description),
|
|
@@ -218,6 +222,7 @@ __decorate([
|
|
|
218
222
|
], VmTemplateController.prototype, "deleteVmTemplate", null);
|
|
219
223
|
__decorate([
|
|
220
224
|
Example(genericAlarmsExample),
|
|
225
|
+
Extension('x-mcp-exposure', 'allow'),
|
|
221
226
|
Get('{id}/alarms'),
|
|
222
227
|
Security('*', ['acl']),
|
|
223
228
|
Tags('alarms'),
|
|
@@ -232,6 +237,7 @@ __decorate([
|
|
|
232
237
|
], VmTemplateController.prototype, "getVmTemplateAlarms", null);
|
|
233
238
|
__decorate([
|
|
234
239
|
Example(vmTemplateVdis),
|
|
240
|
+
Extension('x-mcp-exposure', 'allow'),
|
|
235
241
|
Get('{id}/vdis'),
|
|
236
242
|
Security('*', ['acl']),
|
|
237
243
|
Tags('vdis'),
|
|
@@ -247,6 +253,7 @@ __decorate([
|
|
|
247
253
|
__decorate([
|
|
248
254
|
Example(messageIds),
|
|
249
255
|
Example(partialMessages),
|
|
256
|
+
Extension('x-mcp-exposure', 'allow'),
|
|
250
257
|
Get('{id}/messages'),
|
|
251
258
|
Security('*', ['acl']),
|
|
252
259
|
Tags('messages'),
|
|
@@ -262,6 +269,7 @@ __decorate([
|
|
|
262
269
|
__decorate([
|
|
263
270
|
Example(taskIds),
|
|
264
271
|
Example(partialTasks),
|
|
272
|
+
Extension('x-mcp-exposure', 'allow'),
|
|
265
273
|
Get('{id}/tasks'),
|
|
266
274
|
Security('*', ['acl']),
|
|
267
275
|
Tags('tasks'),
|
|
@@ -275,6 +283,7 @@ __decorate([
|
|
|
275
283
|
__param(6, Query())
|
|
276
284
|
], VmTemplateController.prototype, "getVmTemplateTasks", null);
|
|
277
285
|
__decorate([
|
|
286
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
278
287
|
Put('{id}/tags/{tag}'),
|
|
279
288
|
Middlewares(acl({ resource: 'vm-template', action: 'update:tags', objectId: 'params.id' })),
|
|
280
289
|
SuccessResponse(noContentResp.status, noContentResp.description),
|
|
@@ -284,6 +293,7 @@ __decorate([
|
|
|
284
293
|
__param(1, Path())
|
|
285
294
|
], VmTemplateController.prototype, "putVmTemplateTag", null);
|
|
286
295
|
__decorate([
|
|
296
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
287
297
|
Delete('{id}/tags/{tag}'),
|
|
288
298
|
Middlewares(acl({ resource: 'vm-template', action: 'update:tags', objectId: 'params.id' })),
|
|
289
299
|
SuccessResponse(noContentResp.status, noContentResp.description),
|
|
@@ -7,7 +7,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
8
8
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
9
9
|
};
|
|
10
|
-
import { Example, Get, Path, Post, Query, Request, Response, Route, Security,
|
|
10
|
+
import { Body, Delete, Example, Extension, Get, Middlewares, Path, Post, Put, Query, Request, Response, Route, Security, SuccessResponse, Tags, } from 'tsoa';
|
|
11
11
|
import { json } from 'express';
|
|
12
12
|
import { inject } from 'inversify';
|
|
13
13
|
import { incorrectState, invalidParameters } from 'xo-common/api-errors.js';
|
|
@@ -631,6 +631,7 @@ let VmController = class VmController extends XapiXoController {
|
|
|
631
631
|
__decorate([
|
|
632
632
|
Example(vmIds),
|
|
633
633
|
Example(partialVms),
|
|
634
|
+
Extension('x-mcp-exposure', 'allow'),
|
|
634
635
|
Get(''),
|
|
635
636
|
Security('*', ['acl']),
|
|
636
637
|
__param(0, Request()),
|
|
@@ -641,6 +642,7 @@ __decorate([
|
|
|
641
642
|
__param(5, Query())
|
|
642
643
|
], VmController.prototype, "getVms", null);
|
|
643
644
|
__decorate([
|
|
645
|
+
Extension('x-mcp-exposure', 'deny'),
|
|
644
646
|
Get('{id}.{format}'),
|
|
645
647
|
Middlewares(acl({ resource: 'vm', action: 'export', objectId: 'params.id' })),
|
|
646
648
|
SuccessResponse(200, 'Download started', 'application/octet-stream'),
|
|
@@ -654,6 +656,7 @@ __decorate([
|
|
|
654
656
|
], VmController.prototype, "exportVm", null);
|
|
655
657
|
__decorate([
|
|
656
658
|
Example(vm),
|
|
659
|
+
Extension('x-mcp-exposure', 'allow'),
|
|
657
660
|
Get('{id}'),
|
|
658
661
|
Middlewares(acl({ resource: 'vm', action: 'read', objectId: 'params.id' })),
|
|
659
662
|
Response(forbiddenOperationResp.status, forbiddenOperationResp.description),
|
|
@@ -661,6 +664,7 @@ __decorate([
|
|
|
661
664
|
__param(0, Path())
|
|
662
665
|
], VmController.prototype, "getVm", null);
|
|
663
666
|
__decorate([
|
|
667
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
664
668
|
Delete('{id}'),
|
|
665
669
|
Middlewares(acl({ resource: 'vm', action: 'delete', objectId: 'params.id' })),
|
|
666
670
|
SuccessResponse(noContentResp.status, noContentResp.description),
|
|
@@ -671,6 +675,7 @@ __decorate([
|
|
|
671
675
|
], VmController.prototype, "deleteVm", null);
|
|
672
676
|
__decorate([
|
|
673
677
|
Example(vmStatsExample),
|
|
678
|
+
Extension('x-mcp-exposure', 'deny'),
|
|
674
679
|
Get('{id}/stats'),
|
|
675
680
|
Middlewares(acl({ resource: 'vm', action: 'read', objectId: 'params.id' })),
|
|
676
681
|
Response(forbiddenOperationResp.status, forbiddenOperationResp.description),
|
|
@@ -680,6 +685,7 @@ __decorate([
|
|
|
680
685
|
__param(1, Query())
|
|
681
686
|
], VmController.prototype, "getVmStats", null);
|
|
682
687
|
__decorate([
|
|
688
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
683
689
|
Put('{id}/stats/data_source/{data_source}'),
|
|
684
690
|
Middlewares(acl({ resource: 'vm', action: 'update:datasources', objectId: 'params.id' })),
|
|
685
691
|
SuccessResponse(noContentResp.status, noContentResp.description),
|
|
@@ -690,6 +696,7 @@ __decorate([
|
|
|
690
696
|
__param(1, Path('data_source'))
|
|
691
697
|
], VmController.prototype, "addDataSource", null);
|
|
692
698
|
__decorate([
|
|
699
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
693
700
|
Delete('{id}/stats/data_source/{data_source}'),
|
|
694
701
|
Middlewares(acl({ resource: 'vm', action: 'update:datasources', objectId: 'params.id' })),
|
|
695
702
|
SuccessResponse(noContentResp.status, noContentResp.description),
|
|
@@ -701,6 +708,7 @@ __decorate([
|
|
|
701
708
|
], VmController.prototype, "deleteDataSource", null);
|
|
702
709
|
__decorate([
|
|
703
710
|
Example(taskLocation),
|
|
711
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
704
712
|
Post('{id}/actions/start'),
|
|
705
713
|
Middlewares([
|
|
706
714
|
json(),
|
|
@@ -720,6 +728,7 @@ __decorate([
|
|
|
720
728
|
], VmController.prototype, "startVm", null);
|
|
721
729
|
__decorate([
|
|
722
730
|
Example(taskLocation),
|
|
731
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
723
732
|
Post('{id}/actions/clean_shutdown'),
|
|
724
733
|
Middlewares(acl({ resource: 'vm', action: 'shutdown:clean', objectId: 'params.id' })),
|
|
725
734
|
SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
|
|
@@ -732,6 +741,7 @@ __decorate([
|
|
|
732
741
|
], VmController.prototype, "cleanShutdownVm", null);
|
|
733
742
|
__decorate([
|
|
734
743
|
Example(taskLocation),
|
|
744
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
735
745
|
Post('{id}/actions/clean_reboot'),
|
|
736
746
|
Middlewares(acl({ resource: 'vm', action: 'reboot:clean', objectId: 'params.id' })),
|
|
737
747
|
SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
|
|
@@ -743,6 +753,7 @@ __decorate([
|
|
|
743
753
|
], VmController.prototype, "cleanRebootVm", null);
|
|
744
754
|
__decorate([
|
|
745
755
|
Example(taskLocation),
|
|
756
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
746
757
|
Post('{id}/actions/hard_shutdown'),
|
|
747
758
|
Middlewares(acl({ resource: 'vm', action: 'shutdown:hard', objectId: 'params.id' })),
|
|
748
759
|
SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
|
|
@@ -755,6 +766,7 @@ __decorate([
|
|
|
755
766
|
], VmController.prototype, "hardShutdownVm", null);
|
|
756
767
|
__decorate([
|
|
757
768
|
Example(taskLocation),
|
|
769
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
758
770
|
Post('{id}/actions/hard_reboot'),
|
|
759
771
|
Middlewares(acl({ resource: 'vm', action: 'reboot:hard', objectId: 'params.id' })),
|
|
760
772
|
SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
|
|
@@ -767,6 +779,7 @@ __decorate([
|
|
|
767
779
|
], VmController.prototype, "hardRebootVm", null);
|
|
768
780
|
__decorate([
|
|
769
781
|
Example(taskLocation),
|
|
782
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
770
783
|
Post('{id}/actions/pause'),
|
|
771
784
|
Middlewares(acl({ resource: 'vm', action: 'pause', objectId: 'params.id' })),
|
|
772
785
|
SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
|
|
@@ -779,6 +792,7 @@ __decorate([
|
|
|
779
792
|
], VmController.prototype, "pauseVm", null);
|
|
780
793
|
__decorate([
|
|
781
794
|
Example(taskLocation),
|
|
795
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
782
796
|
Post('{id}/actions/suspend'),
|
|
783
797
|
Middlewares(acl({ resource: 'vm', action: 'suspend', objectId: 'params.id' })),
|
|
784
798
|
SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
|
|
@@ -791,6 +805,7 @@ __decorate([
|
|
|
791
805
|
], VmController.prototype, "suspendVm", null);
|
|
792
806
|
__decorate([
|
|
793
807
|
Example(taskLocation),
|
|
808
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
794
809
|
Post('{id}/actions/resume'),
|
|
795
810
|
Middlewares(acl({ resource: 'vm', action: 'resume', objectId: 'params.id' })),
|
|
796
811
|
SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
|
|
@@ -803,6 +818,7 @@ __decorate([
|
|
|
803
818
|
], VmController.prototype, "resumeVm", null);
|
|
804
819
|
__decorate([
|
|
805
820
|
Example(taskLocation),
|
|
821
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
806
822
|
Post('{id}/actions/unpause'),
|
|
807
823
|
Middlewares(acl({ resource: 'vm', action: 'unpause', objectId: 'params.id' })),
|
|
808
824
|
SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
|
|
@@ -815,6 +831,7 @@ __decorate([
|
|
|
815
831
|
], VmController.prototype, "unpauseVm", null);
|
|
816
832
|
__decorate([
|
|
817
833
|
Example(taskLocation),
|
|
834
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
818
835
|
Post('{id}/actions/revert_snapshot'),
|
|
819
836
|
Middlewares([
|
|
820
837
|
json(),
|
|
@@ -839,6 +856,7 @@ __decorate([
|
|
|
839
856
|
], VmController.prototype, "revertSnapshotVm", null);
|
|
840
857
|
__decorate([
|
|
841
858
|
Example(taskLocation),
|
|
859
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
842
860
|
Post('{id}/actions/snapshot'),
|
|
843
861
|
Middlewares([json(), acl({ resource: 'vm', action: 'snapshot', objectId: 'params.id' })]),
|
|
844
862
|
SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
|
|
@@ -852,6 +870,7 @@ __decorate([
|
|
|
852
870
|
], VmController.prototype, "snapshotVm", null);
|
|
853
871
|
__decorate([
|
|
854
872
|
Example(taskLocation),
|
|
873
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
855
874
|
Post('{id}/actions/clone'),
|
|
856
875
|
Middlewares(json()),
|
|
857
876
|
SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
|
|
@@ -865,6 +884,7 @@ __decorate([
|
|
|
865
884
|
], VmController.prototype, "cloneVm", null);
|
|
866
885
|
__decorate([
|
|
867
886
|
Example(genericAlarmsExample),
|
|
887
|
+
Extension('x-mcp-exposure', 'allow'),
|
|
868
888
|
Get('{id}/alarms'),
|
|
869
889
|
Security('*', ['acl']),
|
|
870
890
|
Tags('alarms'),
|
|
@@ -879,6 +899,7 @@ __decorate([
|
|
|
879
899
|
], VmController.prototype, "getVmAlarms", null);
|
|
880
900
|
__decorate([
|
|
881
901
|
Example(vmVdis),
|
|
902
|
+
Extension('x-mcp-exposure', 'allow'),
|
|
882
903
|
Get('{id}/vdis'),
|
|
883
904
|
Security('*', ['acl']),
|
|
884
905
|
Tags('vdis'),
|
|
@@ -894,6 +915,7 @@ __decorate([
|
|
|
894
915
|
__decorate([
|
|
895
916
|
Example(vmBackupJobIds),
|
|
896
917
|
Example(partialVmBackupJobs),
|
|
918
|
+
Extension('x-mcp-exposure', 'allow'),
|
|
897
919
|
Get('{id}/backup-jobs'),
|
|
898
920
|
Security('*', ['acl']),
|
|
899
921
|
Tags('backup-jobs'),
|
|
@@ -909,6 +931,7 @@ __decorate([
|
|
|
909
931
|
__decorate([
|
|
910
932
|
Example(messageIds),
|
|
911
933
|
Example(partialMessages),
|
|
934
|
+
Extension('x-mcp-exposure', 'allow'),
|
|
912
935
|
Get('{id}/messages'),
|
|
913
936
|
Security('*', ['acl']),
|
|
914
937
|
Tags('messages'),
|
|
@@ -924,6 +947,7 @@ __decorate([
|
|
|
924
947
|
__decorate([
|
|
925
948
|
Example(taskIds),
|
|
926
949
|
Example(partialTasks),
|
|
950
|
+
Extension('x-mcp-exposure', 'allow'),
|
|
927
951
|
Get('{id}/tasks'),
|
|
928
952
|
Security('*', ['acl']),
|
|
929
953
|
Tags('tasks'),
|
|
@@ -937,6 +961,7 @@ __decorate([
|
|
|
937
961
|
__param(6, Query())
|
|
938
962
|
], VmController.prototype, "getVmTasks", null);
|
|
939
963
|
__decorate([
|
|
964
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
940
965
|
Put('{id}/tags/{tag}'),
|
|
941
966
|
Middlewares(acl({ resource: 'vm', action: 'update:tags', objectId: 'params.id' })),
|
|
942
967
|
SuccessResponse(noContentResp.status, noContentResp.description),
|
|
@@ -946,6 +971,7 @@ __decorate([
|
|
|
946
971
|
__param(1, Path())
|
|
947
972
|
], VmController.prototype, "putVmTag", null);
|
|
948
973
|
__decorate([
|
|
974
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
949
975
|
Delete('{id}/tags/{tag}'),
|
|
950
976
|
Middlewares(acl({ resource: 'vm', action: 'update:tags', objectId: 'params.id' })),
|
|
951
977
|
SuccessResponse(noContentResp.status, noContentResp.description),
|
|
@@ -956,6 +982,7 @@ __decorate([
|
|
|
956
982
|
], VmController.prototype, "deleteVmTag", null);
|
|
957
983
|
__decorate([
|
|
958
984
|
Example(vmDashboard),
|
|
985
|
+
Extension('x-mcp-exposure', 'allow'),
|
|
959
986
|
Get('{id}/dashboard'),
|
|
960
987
|
Middlewares(acl({ resource: 'vm', action: 'read', objectId: 'params.id' })),
|
|
961
988
|
Response(forbiddenOperationResp.status, forbiddenOperationResp.description),
|
|
@@ -965,6 +992,7 @@ __decorate([
|
|
|
965
992
|
], VmController.prototype, "getVmDashboard", null);
|
|
966
993
|
__decorate([
|
|
967
994
|
Example(taskLocation),
|
|
995
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
968
996
|
Post('{id}/actions/migrate'),
|
|
969
997
|
Middlewares(json()),
|
|
970
998
|
SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
|
|
@@ -7,7 +7,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
8
8
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
9
9
|
};
|
|
10
|
-
import { Controller, Example, Get, Query, Request, Response, Route, Security, Tags } from 'tsoa';
|
|
10
|
+
import { Controller, Example, Extension, Get, Query, Request, Response, Route, Security, Tags } from 'tsoa';
|
|
11
11
|
import { inject } from 'inversify';
|
|
12
12
|
import { PassThrough } from 'node:stream';
|
|
13
13
|
import { provide } from 'inversify-binding-decorators';
|
|
@@ -58,6 +58,7 @@ let XoaController = class XoaController extends Controller {
|
|
|
58
58
|
};
|
|
59
59
|
__decorate([
|
|
60
60
|
Example(xoaDashboard),
|
|
61
|
+
Extension('x-mcp-exposure', 'allow'),
|
|
61
62
|
Get('dashboard'),
|
|
62
63
|
Response(badRequestResp.status, badRequestResp.description),
|
|
63
64
|
Response(unauthorizedResp.status, unauthorizedResp.description),
|
|
@@ -67,11 +68,13 @@ __decorate([
|
|
|
67
68
|
__decorate([
|
|
68
69
|
Security('none'),
|
|
69
70
|
Example(pingResponse),
|
|
71
|
+
Extension('x-mcp-exposure', 'allow'),
|
|
70
72
|
Get('ping')
|
|
71
73
|
], XoaController.prototype, "ping", null);
|
|
72
74
|
__decorate([
|
|
73
75
|
Security('none'),
|
|
74
76
|
Example(guiRoutes),
|
|
77
|
+
Extension('x-mcp-exposure', 'allow'),
|
|
75
78
|
Get('gui-routes')
|
|
76
79
|
], XoaController.prototype, "getGuiRoutes", null);
|
|
77
80
|
XoaController = __decorate([
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Require `@Extension('x-mcp-exposure', 'allow' | 'confirm' | 'deny')` next to
|
|
5
|
+
* every `@Get/@Post/@Put/@Patch/@Delete` method. tsoa detects extension
|
|
6
|
+
* decorators by AST identifier name, so the `Extension` literal must appear in
|
|
7
|
+
* source — a `@McpExpose` wrapper would not be picked up.
|
|
8
|
+
*
|
|
9
|
+
* Scope: this is a SURFACE-CONTROL policy, not a security control. The value
|
|
10
|
+
* only decides which endpoints the official `@xen-orchestra/mcp` server turns
|
|
11
|
+
* into LLM tools; the REST API itself never reads `x-mcp-exposure`. Every
|
|
12
|
+
* endpoint stays gated by its own RBAC/ACL middleware regardless of this
|
|
13
|
+
* annotation, and any REST client with valid credentials (including a
|
|
14
|
+
* non-official MCP) can still call a `'deny'`-tagged endpoint directly. The
|
|
15
|
+
* rule only guarantees the exposure decision is made explicitly per endpoint —
|
|
16
|
+
* it does not harden the REST API.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const HTTP_METHOD_DECORATORS = new Set(['Get', 'Post', 'Put', 'Patch', 'Delete'])
|
|
20
|
+
const MCP_EXPOSURE_KEY = 'x-mcp-exposure'
|
|
21
|
+
const VALID_EXPOSURES = new Set(['allow', 'confirm', 'deny'])
|
|
22
|
+
|
|
23
|
+
function getDecoratorCalleeName(decorator) {
|
|
24
|
+
const expr = decorator.expression
|
|
25
|
+
if (!expr) return undefined
|
|
26
|
+
if (expr.type === 'CallExpression' && expr.callee && expr.callee.type === 'Identifier') {
|
|
27
|
+
return expr.callee.name
|
|
28
|
+
}
|
|
29
|
+
if (expr.type === 'Identifier') {
|
|
30
|
+
return expr.name
|
|
31
|
+
}
|
|
32
|
+
return undefined
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function findHttpMethodDecorator(decorators) {
|
|
36
|
+
for (const decorator of decorators) {
|
|
37
|
+
const name = getDecoratorCalleeName(decorator)
|
|
38
|
+
if (name !== undefined && HTTP_METHOD_DECORATORS.has(name)) {
|
|
39
|
+
return { decorator, name }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return undefined
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getStringLiteralValue(node) {
|
|
46
|
+
if (!node) return undefined
|
|
47
|
+
if (node.type === 'Literal' && typeof node.value === 'string') return node.value
|
|
48
|
+
if (node.type === 'TemplateLiteral' && node.expressions.length === 0 && node.quasis.length === 1) {
|
|
49
|
+
return node.quasis[0].value.cooked
|
|
50
|
+
}
|
|
51
|
+
return undefined
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function findMcpExposureExtension(decorators) {
|
|
55
|
+
for (const decorator of decorators) {
|
|
56
|
+
if (!decorator.expression || decorator.expression.type !== 'CallExpression') continue
|
|
57
|
+
const callee = decorator.expression.callee
|
|
58
|
+
if (!callee || callee.type !== 'Identifier' || callee.name !== 'Extension') continue
|
|
59
|
+
|
|
60
|
+
const [keyArg, valueArg] = decorator.expression.arguments
|
|
61
|
+
const key = getStringLiteralValue(keyArg)
|
|
62
|
+
if (key !== MCP_EXPOSURE_KEY) continue
|
|
63
|
+
|
|
64
|
+
return { decorator, valueArg }
|
|
65
|
+
}
|
|
66
|
+
return undefined
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getMethodName(methodNode) {
|
|
70
|
+
if (!methodNode.key) return '<anonymous>'
|
|
71
|
+
if (methodNode.key.type === 'Identifier') return methodNode.key.name
|
|
72
|
+
if (methodNode.key.type === 'Literal') return String(methodNode.key.value)
|
|
73
|
+
return '<computed>'
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = {
|
|
77
|
+
meta: {
|
|
78
|
+
type: 'problem',
|
|
79
|
+
docs: {
|
|
80
|
+
description:
|
|
81
|
+
'Every REST endpoint must declare its MCP exposure with @Extension("x-mcp-exposure", "allow" | "confirm" | "deny").',
|
|
82
|
+
},
|
|
83
|
+
schema: [],
|
|
84
|
+
messages: {
|
|
85
|
+
missingMcpExpose:
|
|
86
|
+
'Endpoint "{{name}}" decorated with @{{httpMethod}} is missing an @Extension("x-mcp-exposure", ...) decorator. ' +
|
|
87
|
+
'Add @Extension("x-mcp-exposure", "allow"), "confirm", or "deny" to make the MCP exposure decision explicit.',
|
|
88
|
+
invalidMcpExpose:
|
|
89
|
+
'Endpoint "{{name}}" has @Extension("x-mcp-exposure", ...) with an invalid value. ' +
|
|
90
|
+
'Expected one of: "allow", "confirm", "deny". Got: {{got}}.',
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
create(context) {
|
|
94
|
+
return {
|
|
95
|
+
MethodDefinition(node) {
|
|
96
|
+
if (node.kind === 'constructor') return
|
|
97
|
+
const decorators = node.decorators
|
|
98
|
+
if (!decorators || decorators.length === 0) return
|
|
99
|
+
|
|
100
|
+
const httpMethod = findHttpMethodDecorator(decorators)
|
|
101
|
+
if (!httpMethod) return
|
|
102
|
+
|
|
103
|
+
const methodName = getMethodName(node)
|
|
104
|
+
const mcpExposure = findMcpExposureExtension(decorators)
|
|
105
|
+
|
|
106
|
+
if (!mcpExposure) {
|
|
107
|
+
context.report({
|
|
108
|
+
node,
|
|
109
|
+
messageId: 'missingMcpExpose',
|
|
110
|
+
data: { name: methodName, httpMethod: httpMethod.name },
|
|
111
|
+
})
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const exposureValue = getStringLiteralValue(mcpExposure.valueArg)
|
|
116
|
+
if (exposureValue === undefined || !VALID_EXPOSURES.has(exposureValue)) {
|
|
117
|
+
context.report({
|
|
118
|
+
node: mcpExposure.valueArg ?? mcpExposure.decorator,
|
|
119
|
+
messageId: 'invalidMcpExpose',
|
|
120
|
+
data: {
|
|
121
|
+
name: methodName,
|
|
122
|
+
got: exposureValue === undefined ? '<non-literal>' : JSON.stringify(exposureValue),
|
|
123
|
+
},
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
}
|