@xen-orchestra/rest-api 0.31.0 → 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.
Files changed (44) hide show
  1. package/dist/acl-privileges/acl-privilege.controller.mjs +7 -2
  2. package/dist/acl-roles/acl-role.controller.mjs +13 -2
  3. package/dist/alarms/alarm.controller.mjs +3 -1
  4. package/dist/backup-archives/backup-archive.controller.mjs +3 -1
  5. package/dist/backup-jobs/backup-job.controller.mjs +12 -1
  6. package/dist/backup-logs/backup-log.controller.mjs +3 -1
  7. package/dist/backup-repositories/backup-repositories.controller.mjs +3 -1
  8. package/dist/events/event.controller.mjs +4 -1
  9. package/dist/groups/group.controller.mjs +10 -1
  10. package/dist/hosts/host.controller.mjs +16 -1
  11. package/dist/index.mjs +2 -0
  12. package/dist/mcp/mcp.controller.mjs +59 -0
  13. package/dist/mcp/mcp.helper.mjs +11 -0
  14. package/dist/messages/message.controller.mjs +3 -1
  15. package/dist/middlewares/mcp-gate.middleware.mjs +30 -0
  16. package/dist/networks/network.controller.mjs +9 -1
  17. package/dist/open-api/routes/routes.js +33 -1
  18. package/dist/pbds/pbd.controller.mjs +5 -1
  19. package/dist/pcis/pci.controller.mjs +3 -1
  20. package/dist/pgpus/pgpu.controller.mjs +3 -1
  21. package/dist/pifs/pif.controller.mjs +6 -1
  22. package/dist/pools/pool.controller.mjs +20 -1
  23. package/dist/proxies/proxy.controller.mjs +3 -1
  24. package/dist/restore-logs/restore-log.controller.mjs +5 -1
  25. package/dist/schedules/schedule.controller.mjs +4 -1
  26. package/dist/servers/server.controller.mjs +8 -1
  27. package/dist/sms/sm.controller.mjs +3 -1
  28. package/dist/srs/sr.controller.mjs +13 -1
  29. package/dist/tasks/task.controller.mjs +6 -1
  30. package/dist/users/user.controller.mjs +13 -2
  31. package/dist/vbds/vbd.controller.mjs +10 -1
  32. package/dist/vdi-snapshots/vdi-snapshot.controller.mjs +10 -1
  33. package/dist/vdis/vdi.controller.mjs +13 -1
  34. package/dist/vifs/vif.controller.mjs +10 -1
  35. package/dist/vm-controller/vm-controller.controller.mjs +9 -1
  36. package/dist/vm-snapshots/vm-snapshot.controller.mjs +11 -1
  37. package/dist/vm-templates/vm-template.controller.mjs +11 -1
  38. package/dist/vms/vm.controller.mjs +29 -1
  39. package/dist/xoa/xoa.controller.mjs +4 -1
  40. package/eslint-rules/index.cjs +7 -0
  41. package/eslint-rules/require-mcp-expose.cjs +129 -0
  42. package/open-api/spec/swagger.json +563 -264
  43. package/package.json +3 -3
  44. 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, Middlewares, } from 'tsoa';
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, Security, Query, Request, Response, Route, Tags, Path, Delete, SuccessResponse, Put, Middlewares, } from 'tsoa';
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, Tags, SuccessResponse, Body, Put, Delete, Middlewares, } from 'tsoa';
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,7 @@
1
+ 'use strict'
2
+
3
+ module.exports = {
4
+ rules: {
5
+ 'require-mcp-expose': require('./require-mcp-expose.cjs'),
6
+ },
7
+ }
@@ -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
+ }