@adiholick/nvstudio-mcp 2.2.3 → 2.4.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 (42) hide show
  1. package/README.md +57 -47
  2. package/agent-plugin/mcp_config.json +8 -0
  3. package/agent-plugin/plugin.json +6 -0
  4. package/agent-plugin/rules/AGENTS.md +118 -0
  5. package/agent-plugin/skills/nvstudio-animation-vfx/SKILL.md +120 -0
  6. package/agent-plugin/skills/nvstudio-assets-terrain-guide/SKILL.md +27 -0
  7. package/agent-plugin/skills/nvstudio-gui-standards/SKILL.md +419 -0
  8. package/agent-plugin/skills/nvstudio-mcp-guide/SKILL.md +83 -0
  9. package/agent-plugin/skills/nvstudio-rig-engine/SKILL.md +170 -0
  10. package/agent-plugin/skills/nvstudio-roblox-sync-guide/SKILL.md +16 -0
  11. package/dist/build-plugin.js +55 -0
  12. package/dist/index.js +841 -3
  13. package/dist/luau-validator.js +99 -34
  14. package/dist/roblox-bridge.js +152 -4
  15. package/dist/roblox-docs.js +264 -0
  16. package/dist/vfx-database.js +507 -0
  17. package/package.json +6 -4
  18. package/public/index.html +1590 -180
  19. package/studio-plugin/Main.server.lua +1 -1
  20. package/studio-plugin/animate_character.lua +36 -0
  21. package/studio-plugin/apply_vfx.lua +59 -0
  22. package/studio-plugin/batch_create_instances.lua +128 -0
  23. package/studio-plugin/batch_set_properties.lua +69 -0
  24. package/studio-plugin/edit_rig.lua +35 -0
  25. package/studio-plugin/insert_asset.lua +85 -12
  26. package/studio-plugin/nv_anim_dispatcher.lua +239 -0
  27. package/studio-plugin/nv_auto_rigger.lua +392 -0
  28. package/studio-plugin/nv_curves.lua +260 -0
  29. package/studio-plugin/nv_joint_math.lua +138 -0
  30. package/studio-plugin/nv_kinematics.lua +248 -0
  31. package/studio-plugin/nv_motion_generator.lua +279 -0
  32. package/studio-plugin/nv_rig_dispatcher.lua +319 -0
  33. package/studio-plugin/nv_vfx_serializer.lua +363 -0
  34. package/studio-plugin/nvstudio_mcp.rbxmx +3824 -60
  35. package/studio-plugin/preview_asset.lua +99 -0
  36. package/studio-plugin/scan_security.lua +81 -0
  37. package/studio-plugin/scan_vfx.lua +85 -0
  38. package/studio-plugin/scrub_timeline.lua +122 -0
  39. package/studio-plugin/security_scanner.lua +346 -0
  40. package/studio-plugin/set_properties.lua +83 -0
  41. package/studio-plugin/solve_limb_ik.lua +63 -0
  42. package/studio-plugin/ui_manager.lua +1 -1
package/dist/index.js CHANGED
@@ -12,6 +12,8 @@ const task_queue_1 = require("./task-queue");
12
12
  const fs_1 = __importDefault(require("fs"));
13
13
  const path_1 = __importDefault(require("path"));
14
14
  const luau_validator_1 = require("./luau-validator");
15
+ const roblox_docs_1 = require("./roblox-docs");
16
+ const vfx_database_1 = require("./vfx-database");
15
17
  const HISTORY_DIR = path_1.default.join(__dirname, '..', '.history');
16
18
  if (!fs_1.default.existsSync(HISTORY_DIR)) {
17
19
  fs_1.default.mkdirSync(HISTORY_DIR, { recursive: true });
@@ -377,9 +379,64 @@ const TOOLS = [
377
379
  required: ["material", "position", "size"],
378
380
  },
379
381
  },
382
+ {
383
+ name: "preview_asset",
384
+ description: "Pratinjau hierarki dan pemindaian keamanan aset Creator Store secara terisolasi sebelum dimasukkan ke dalam game. Mendeteksi skrip backdoor, require eksternal numerik, obfuscation, webhook discord, dan virus script tanpa risiko mengeksekusi kode berbahaya.",
385
+ inputSchema: {
386
+ type: "object",
387
+ properties: {
388
+ asset_id: {
389
+ type: "string",
390
+ description: "Roblox Asset ID angka (atau rbxassetid://) dari Creator Store.",
391
+ },
392
+ deep_scan: {
393
+ type: "boolean",
394
+ description: "Jika true (default), memindai dan menganalisis source code seluruh skrip di dalam model.",
395
+ },
396
+ placeId: {
397
+ type: "number",
398
+ description: "Target Place ID untuk multi-Studio routing.",
399
+ },
400
+ studioId: {
401
+ type: "string",
402
+ description: "Target Studio session ID.",
403
+ },
404
+ },
405
+ required: ["asset_id"],
406
+ },
407
+ },
408
+ {
409
+ name: "scan_security",
410
+ description: "Audit keamanan instance atau model yang sudah ada di dalam game (Workspace, ServerScriptService, dll.) untuk mendeteksi skrip backdoor, virus, webhook discord, dan kode berbahaya.",
411
+ inputSchema: {
412
+ type: "object",
413
+ properties: {
414
+ target: {
415
+ type: "string",
416
+ description: "Path instance atau folder yang ingin diaudit (default: 'Workspace').",
417
+ },
418
+ deep_scan: {
419
+ type: "boolean",
420
+ description: "Jika true (default), memindai isi source code skrip.",
421
+ },
422
+ clean_threats: {
423
+ type: "boolean",
424
+ description: "Jika true, secara otomatis menghapus skrip berbahaya yang terdeteksi (default: false).",
425
+ },
426
+ placeId: {
427
+ type: "number",
428
+ description: "Target Place ID untuk multi-Studio routing.",
429
+ },
430
+ studioId: {
431
+ type: "string",
432
+ description: "Target Studio session ID.",
433
+ },
434
+ },
435
+ },
436
+ },
380
437
  {
381
438
  name: "insert_asset",
382
- description: "Muat dan masukkan aset Roblox (Model/Package) berdasarkan Asset ID ke dalam Workspace atau target tertentu.",
439
+ description: "Muat dan masukkan aset Roblox (Model/Package) berdasarkan Asset ID ke dalam Workspace atau target tertentu dengan proteksi sanitasi skrip otomatis.",
383
440
  inputSchema: {
384
441
  type: "object",
385
442
  properties: {
@@ -391,6 +448,14 @@ const TOOLS = [
391
448
  type: "string",
392
449
  description: "Parent tujuan tempat menempatkan model (default: 'Workspace').",
393
450
  },
451
+ strip_scripts: {
452
+ type: "boolean",
453
+ description: "Jika true (default: true), secara otomatis menghapus semua skrip dari model sebelum dimasukkan ke game demi keamanan mutlak dari backdoor.",
454
+ },
455
+ scan_security: {
456
+ type: "boolean",
457
+ description: "Jika true (default: true), memindai potensi ancaman keamanan sebelum insert.",
458
+ },
394
459
  placeId: {
395
460
  type: "number",
396
461
  description: "Target Place ID untuk multi-Studio routing.",
@@ -475,7 +540,7 @@ const TOOLS = [
475
540
  },
476
541
  {
477
542
  name: "call_mcp_tool",
478
- description: "Legacy tool dispatcher untuk kompatibilitas mundur. Mendukung semua command: execute_luau, get_children, get_full_tree, get_script_source, update_script_source, patch_script_source, rollback_script, get_properties, set_properties, create_instance, delete_instance, search_instance, generate_terrain, insert_asset, search_asset, script_grep, get_logs.",
543
+ description: "Legacy tool dispatcher untuk kompatibilitas mundur. Mendukung semua command: execute_luau, get_children, get_full_tree, get_script_source, update_script_source, patch_script_source, rollback_script, get_properties, set_properties, create_instance, delete_instance, search_instance, generate_terrain, insert_asset, preview_asset, scan_security, batch_create_instances, batch_set_properties, search_asset, script_grep, get_roblox_docs, get_logs.",
479
544
  inputSchema: {
480
545
  type: "object",
481
546
  properties: {
@@ -503,6 +568,446 @@ const TOOLS = [
503
568
  required: ["command"],
504
569
  },
505
570
  },
571
+ {
572
+ name: "get_roblox_docs",
573
+ description: "Ambil dokumentasi resmi Roblox Engine API langsung dari Creator Docs (Classes, Services, Data types, Enums, dan Libraries). Mengembalikan format Markdown lengkap beserta signature fungsi, deskripsi parameter, properti, event, dan contoh kode untuk mencegah halusinasi kode Luau.",
574
+ inputSchema: {
575
+ type: "object",
576
+ properties: {
577
+ query: {
578
+ type: "string",
579
+ description: "Nama Class, Service, Data type, Enum, atau Library Roblox (contoh: 'TweenService', 'CanvasGroup', 'Vector3', 'EasingStyle', 'task').",
580
+ },
581
+ member: {
582
+ type: "string",
583
+ description: "Opsional: Nama method, property, event, atau enum item spesifik untuk menghemat token (contoh: 'Create', 'GroupTransparency', 'Completed').",
584
+ },
585
+ category: {
586
+ type: "string",
587
+ enum: ["class", "datatype", "enum", "library", "auto"],
588
+ description: "Opsional: Kategori dokumentasi (default: 'auto').",
589
+ },
590
+ },
591
+ required: ["query"],
592
+ },
593
+ },
594
+ {
595
+ name: "batch_create_instances",
596
+ description: "Pembangun instance massal dan deklaratif dalam satu kali pemanggilan. Mendukung mode hierarki bersarang (tree) atau daftar array datar (instances) lengkap dengan penyetelan properti langsung (UDim, Vector2, UDim2, Color3, Enum, dll.) untuk menghemat token dan latensi jaringan.",
597
+ inputSchema: {
598
+ type: "object",
599
+ properties: {
600
+ target: {
601
+ type: "string",
602
+ description: "Path root tujuan tempat menaruh instance (contoh: 'StarterGui.MyScreen', 'Workspace.Map').",
603
+ },
604
+ tree: {
605
+ type: "object",
606
+ description: "Objek hierarki bersarang { className, name, properties, children: [...] } untuk membangun struktur pohon secara rekursif.",
607
+ },
608
+ instances: {
609
+ type: "array",
610
+ description: "Daftar array datar [{ className, name, parent, properties }] jika tidak menggunakan mode tree.",
611
+ },
612
+ placeId: {
613
+ type: "number",
614
+ description: "Target Place ID untuk multi-Studio routing.",
615
+ },
616
+ studioId: {
617
+ type: "string",
618
+ description: "Target Studio session ID.",
619
+ },
620
+ },
621
+ },
622
+ },
623
+ {
624
+ name: "batch_set_properties",
625
+ description: "Ubah properti pada banyak instance sekaligus dalam satu kali request atomik.",
626
+ inputSchema: {
627
+ type: "object",
628
+ properties: {
629
+ updates: {
630
+ type: "array",
631
+ description: "Array objek pembaruan: [{ target: 'Path.To.Instance', properties: { PropName: value, ... } }].",
632
+ },
633
+ placeId: {
634
+ type: "number",
635
+ description: "Target Place ID untuk multi-Studio routing.",
636
+ },
637
+ studioId: {
638
+ type: "string",
639
+ description: "Target Studio session ID.",
640
+ },
641
+ },
642
+ required: ["updates"],
643
+ },
644
+ },
645
+ {
646
+ name: "scan_vfx",
647
+ description: "Pindai efek visual (ParticleEmitter, Beam, Trail, Light) pada objek di Roblox Studio dan simpan ke database persisten MCP untuk digunakan lintas-proyek.",
648
+ inputSchema: {
649
+ type: "object",
650
+ properties: {
651
+ target: {
652
+ type: "string",
653
+ description: "Path objek target di Roblox Studio (contoh: 'Workspace.MagicSpell', 'ReplicatedStorage.SlashFX').",
654
+ },
655
+ name: {
656
+ type: "string",
657
+ description: "Nama untuk efek visual ini di database MCP (opsional, default: nama objek).",
658
+ },
659
+ category: {
660
+ type: "string",
661
+ description: "Kategori efek visual (contoh: 'Slash', 'Explosion', 'Aura', 'Projectile', 'Magic', 'Impact', 'Beam', 'Ambient').",
662
+ enum: ["Slash", "Explosion", "Aura", "Projectile", "Magic", "Impact", "Beam", "Ambient"],
663
+ },
664
+ tags: {
665
+ type: "array",
666
+ items: { type: "string" },
667
+ description: "Daftar kata kunci pencarian (contoh: ['sword', 'cyan', 'fast']).",
668
+ },
669
+ description: {
670
+ type: "string",
671
+ description: "Catatan penjelasan tentang efek visual ini.",
672
+ },
673
+ placeId: {
674
+ type: "number",
675
+ description: "Target Place ID untuk multi-Studio routing.",
676
+ },
677
+ studioId: {
678
+ type: "string",
679
+ description: "Target Studio session ID.",
680
+ },
681
+ },
682
+ required: ["target"],
683
+ },
684
+ },
685
+ {
686
+ name: "apply_vfx",
687
+ description: "Terapkan efek visual yang tersimpan di database persisten MCP ke objek target di Roblox Studio aktif dengan fidelitas 100%.",
688
+ inputSchema: {
689
+ type: "object",
690
+ properties: {
691
+ vfx_id: {
692
+ type: "string",
693
+ description: "ID efek visual dari database MCP (contoh: 'preset-cyan-slash', atau ID hasil scan).",
694
+ },
695
+ target: {
696
+ type: "string",
697
+ description: "Path objek tujuan di Studio tempat VFX akan dipasang (contoh: 'Workspace.Player.RightHand', 'Workspace.Map').",
698
+ },
699
+ scale: {
700
+ type: "number",
701
+ description: "Faktor skala ukuran efek visual (contoh: 1.0 = normal, 1.5 = 50% lebih besar, 0.5 = separuh). Default: 1.0.",
702
+ },
703
+ enabled: {
704
+ type: "boolean",
705
+ description: "Paksa status Enabled pada seluruh emitter/beam (opsional).",
706
+ },
707
+ placeId: {
708
+ type: "number",
709
+ description: "Target Place ID untuk multi-Studio routing.",
710
+ },
711
+ studioId: {
712
+ type: "string",
713
+ description: "Target Studio session ID.",
714
+ },
715
+ },
716
+ required: ["vfx_id", "target"],
717
+ },
718
+ },
719
+ {
720
+ name: "list_vfx_database",
721
+ description: "Lihat katalog efek visual yang tersimpan di database persisten MCP, lengkap dengan rincian node emitter/beam dan filter kategori.",
722
+ inputSchema: {
723
+ type: "object",
724
+ properties: {
725
+ category: {
726
+ type: "string",
727
+ description: "Filter berdasarkan kategori (contoh: 'Slash', 'Aura', 'Explosion', 'Magic', 'Beam', 'Impact').",
728
+ },
729
+ query: {
730
+ type: "string",
731
+ description: "Filter pencarian kata kunci berdasarkan nama, tag, atau deskripsi.",
732
+ },
733
+ },
734
+ },
735
+ },
736
+ {
737
+ name: "delete_vfx",
738
+ description: "Hapus efek visual dari database persisten MCP berdasarkan ID.",
739
+ inputSchema: {
740
+ type: "object",
741
+ properties: {
742
+ id: {
743
+ type: "string",
744
+ description: "ID efek visual yang akan dihapus dari database MCP.",
745
+ },
746
+ },
747
+ required: ["id"],
748
+ },
749
+ },
750
+ {
751
+ name: "animate_character",
752
+ description: "Buat animasi karakter yang halus (Walk, Slash, Punch, Idle, atau Custom) menggunakan NV-Animation Engine (Analytical LimbIK, Bezier Curves, Easing, dan Floor Pinning), lalu simpan sebagai KeyframeSequence resmi.",
753
+ inputSchema: {
754
+ type: "object",
755
+ properties: {
756
+ target: {
757
+ type: "string",
758
+ description: "Path model karakter rig R6 atau R15 di Studio (contoh: 'Workspace.Dummy', 'Workspace.Zombie').",
759
+ },
760
+ action: {
761
+ type: "string",
762
+ description: "Jenis animasi yang ingin dibuat.",
763
+ enum: ["walk_cycle", "melee_slash", "punch", "idle_breath", "custom"],
764
+ },
765
+ parameters: {
766
+ type: "object",
767
+ description: "Parameter kustomisasi animasi: { duration: number, intensity: number, easing: string, loop: boolean, clipName: string }.",
768
+ },
769
+ key_poses: {
770
+ type: "array",
771
+ description: "Array pose kunci jika menggunakan action 'custom': [{ time: 0, poses: { MotorName: { angles: [X,Y,Z] } } }].",
772
+ },
773
+ placeId: {
774
+ type: "number",
775
+ description: "Target Place ID untuk multi-Studio routing.",
776
+ },
777
+ studioId: {
778
+ type: "string",
779
+ description: "Target Studio session ID.",
780
+ },
781
+ },
782
+ required: ["target", "action"],
783
+ },
784
+ },
785
+ {
786
+ name: "solve_limb_ik",
787
+ description: "Hitung dan terapkan rotasi sendi lengan/kaki (analytical 2-link LimbIK O(1)) menuju koordinat sasaran 3D secara instan dan halus.",
788
+ inputSchema: {
789
+ type: "object",
790
+ properties: {
791
+ target: {
792
+ type: "string",
793
+ description: "Path model karakter rig R6 atau R15 di Studio (contoh: 'Workspace.Dummy').",
794
+ },
795
+ limb: {
796
+ type: "string",
797
+ description: "Anggota tubuh yang ingin digerakkan.",
798
+ enum: ["LeftArm", "RightArm", "LeftLeg", "RightLeg"],
799
+ },
800
+ goal: {
801
+ type: "array",
802
+ items: { type: "number" },
803
+ description: "Target posisi 3D di world space: [X, Y, Z].",
804
+ },
805
+ pole: {
806
+ type: "array",
807
+ items: { type: "number" },
808
+ description: "Arah tekukan siku/lutut (pole vector 3D): [X, Y, Z] (opsional).",
809
+ },
810
+ apply_to_rig: {
811
+ type: "boolean",
812
+ description: "Apakah orientasi sendi langsung diterapkan ke model karakter di Studio (default: true).",
813
+ },
814
+ placeId: {
815
+ type: "number",
816
+ description: "Target Place ID untuk multi-Studio routing.",
817
+ },
818
+ studioId: {
819
+ type: "string",
820
+ description: "Target Studio session ID.",
821
+ },
822
+ },
823
+ required: ["target", "limb", "goal"],
824
+ },
825
+ },
826
+ {
827
+ name: "scrub_timeline",
828
+ description: "Lakukan scrubbing playhead timeline animasi dan efek visual ke timestamp tertentu (t) secara langsung di Roblox Studio untuk melihat pose karakter dan memicu burst partikel VFX.",
829
+ inputSchema: {
830
+ type: "object",
831
+ properties: {
832
+ target: {
833
+ type: "string",
834
+ description: "Path objek target di Studio (Model karakter atau folder VFX).",
835
+ },
836
+ time: {
837
+ type: "number",
838
+ description: "Posisi waktu playhead timeline dalam detik (contoh: 0.35).",
839
+ },
840
+ action: {
841
+ type: "string",
842
+ description: "Aksi animasi yang ingin di-scrub (contoh: 'melee_slash', 'walk_cycle', 'punch', 'idle_breath').",
843
+ },
844
+ emit_vfx: {
845
+ type: "boolean",
846
+ description: "Apakah memicu semburan (burst) partikel pada frame waktu ini (default: true).",
847
+ },
848
+ emit_count: {
849
+ type: "number",
850
+ description: "Jumlah partikel yang disemburkan (default: 20).",
851
+ },
852
+ enabled: {
853
+ type: "boolean",
854
+ description: "Paksa status Enabled pada Beam/Trail pada playhead waktu ini.",
855
+ },
856
+ placeId: {
857
+ type: "number",
858
+ description: "Target Place ID untuk multi-Studio routing.",
859
+ },
860
+ studioId: {
861
+ type: "string",
862
+ description: "Target Studio session ID.",
863
+ },
864
+ },
865
+ required: ["target", "time"],
866
+ },
867
+ },
868
+ {
869
+ name: "inspect_rig",
870
+ description: "Inspeksi topologi dan hierarki rig karakter/objek di Studio secara headless. Menganalisis pohon sendi (Motor6D & Weld), mendeteksi circular dependency, memeriksa status anchor, dan melaporkan posisi poros di dunia 3D.",
871
+ inputSchema: {
872
+ type: "object",
873
+ properties: {
874
+ target: {
875
+ type: "string",
876
+ description: "Path objek Model karakter atau objek di Studio (contoh: 'Workspace.Dummy').",
877
+ },
878
+ placeId: { type: "number", description: "Target Place ID untuk multi-Studio routing." },
879
+ studioId: { type: "string", description: "Target Studio session ID." },
880
+ },
881
+ required: ["target"],
882
+ },
883
+ },
884
+ {
885
+ name: "edit_rig_joint",
886
+ description: "Operasi headless sendi NV-Rig Engine: buat Motor6D/Weld baru, geser titik tumpu (pivot) tanpa mendislokasi part anak, putar orientasi poros, setel ulang (reset), selaraskan (align), atau hapus sendi.",
887
+ inputSchema: {
888
+ type: "object",
889
+ properties: {
890
+ target: {
891
+ type: "string",
892
+ description: "Path Model karakter/objek di Studio (contoh: 'Workspace.Dummy').",
893
+ },
894
+ action: {
895
+ type: "string",
896
+ description: "Aksi manipulasi sendi.",
897
+ enum: ["create_joint", "create_weld", "edit_pivot", "rotate_pivot", "reset_pivot", "align_joint", "remove_joint"],
898
+ },
899
+ joint_name: {
900
+ type: "string",
901
+ description: "Nama sendi yang ingin diedit/dihapus (contoh: 'RightShoulder', 'Neck').",
902
+ },
903
+ part0: {
904
+ type: "string",
905
+ description: "Nama part induk (Part0) untuk create_joint/create_weld (contoh: 'Torso', 'UpperTorso').",
906
+ },
907
+ part1: {
908
+ type: "string",
909
+ description: "Nama part anak (Part1) untuk create_joint/create_weld (contoh: 'Right Arm', 'RightUpperArm').",
910
+ },
911
+ delta_world: {
912
+ type: "array",
913
+ items: { type: "number" },
914
+ description: "Pergeseran poros di koordinat dunia [X, Y, Z] untuk edit_pivot.",
915
+ },
916
+ target_world: {
917
+ type: "array",
918
+ items: { type: "number" },
919
+ description: "Posisi absolut poros target di dunia [X, Y, Z] untuk edit_pivot.",
920
+ },
921
+ rotate_euler_deg: {
922
+ type: "array",
923
+ items: { type: "number" },
924
+ description: "Rotasi poros [Pitch, Yaw, Roll] dalam derajat untuk rotate_pivot.",
925
+ },
926
+ align_to: {
927
+ type: "string",
928
+ description: "Penyelarasan orientasi poros ke 'Part0' atau 'Part1' untuk align_joint.",
929
+ enum: ["Part0", "Part1"],
930
+ },
931
+ reanchor: {
932
+ type: "boolean",
933
+ description: "Setel Part1 kembali ke 'Anchored = true' saat menghapus sendi (opsional).",
934
+ },
935
+ placeId: { type: "number", description: "Target Place ID untuk multi-Studio routing." },
936
+ studioId: { type: "string", description: "Target Studio session ID." },
937
+ },
938
+ required: ["target", "action"],
939
+ },
940
+ },
941
+ {
942
+ name: "auto_rig",
943
+ description: "Auto-rigging cerdas 1-klik untuk Avatar R6, R15, atau Penautan Senjata (Weapon/Tool) dengan matematika kompensasi spasial otomatis.",
944
+ inputSchema: {
945
+ type: "object",
946
+ properties: {
947
+ target: {
948
+ type: "string",
949
+ description: "Path Model karakter di Studio (contoh: 'Workspace.Dummy', 'Workspace.CustomAvatar').",
950
+ },
951
+ rig_type: {
952
+ type: "string",
953
+ description: "Tipe rig yang ingin dibangun secara otomatis.",
954
+ enum: ["R6", "R15", "Weapon"],
955
+ },
956
+ weapon_target: {
957
+ type: "string",
958
+ description: "Path objek senjata/prop di Studio jika rig_type='Weapon' (contoh: 'Workspace.Sword').",
959
+ },
960
+ target_limb: {
961
+ type: "string",
962
+ description: "Anggota tubuh tempat senjata dipasang (contoh: 'RightHand' atau 'Right Arm').",
963
+ },
964
+ placeId: { type: "number", description: "Target Place ID untuk multi-Studio routing." },
965
+ studioId: { type: "string", description: "Target Studio session ID." },
966
+ },
967
+ required: ["target", "rig_type"],
968
+ },
969
+ },
970
+ {
971
+ name: "weld_assembly",
972
+ description: "Las (Weld) seluruh komponen BasePart pada sebuah model (aksesori, armor, prop, kendaraan) ke PrimaryPart secara otomatis dengan unanchor aman.",
973
+ inputSchema: {
974
+ type: "object",
975
+ properties: {
976
+ target: {
977
+ type: "string",
978
+ description: "Path Model atau Folder di Studio yang ingin dilas (contoh: 'Workspace.ArmorSet').",
979
+ },
980
+ primary_part: {
981
+ type: "string",
982
+ description: "Nama part utama sebagai jangkar (opsional, jika kosong menggunakan PrimaryPart atau part terbesar).",
983
+ },
984
+ placeId: { type: "number", description: "Target Place ID untuk multi-Studio routing." },
985
+ studioId: { type: "string", description: "Target Studio session ID." },
986
+ },
987
+ required: ["target"],
988
+ },
989
+ },
990
+ {
991
+ name: "mirror_rig",
992
+ description: "Cerminkan sambungan sendi bilateral (Left <-> Right) sepanjang sumbu simetri X secara matematis, menghemat waktu konfigurasi karakter.",
993
+ inputSchema: {
994
+ type: "object",
995
+ properties: {
996
+ target: {
997
+ type: "string",
998
+ description: "Path Model karakter di Studio (contoh: 'Workspace.Dummy').",
999
+ },
1000
+ source_side: {
1001
+ type: "string",
1002
+ description: "Sisi sumber yang ingin dicerminkan.",
1003
+ enum: ["Left", "Right"],
1004
+ },
1005
+ placeId: { type: "number", description: "Target Place ID untuk multi-Studio routing." },
1006
+ studioId: { type: "string", description: "Target Studio session ID." },
1007
+ },
1008
+ required: ["target"],
1009
+ },
1010
+ },
506
1011
  ];
507
1012
  server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
508
1013
  return { tools: TOOLS };
@@ -752,11 +1257,41 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
752
1257
  });
753
1258
  break;
754
1259
  }
1260
+ case "preview_asset": {
1261
+ const assetId = String(args.asset_id ?? "");
1262
+ if (!assetId)
1263
+ throw new Error("Parameter 'asset_id' wajib diisi.");
1264
+ const payload = JSON.stringify({
1265
+ asset_id: assetId,
1266
+ deep_scan: args.deep_scan !== false,
1267
+ });
1268
+ result = await dispatchTaskToStudio("preview_asset", "", payload, {
1269
+ placeId: args.placeId,
1270
+ studioId: args.studioId,
1271
+ });
1272
+ break;
1273
+ }
1274
+ case "scan_security": {
1275
+ const payload = JSON.stringify({
1276
+ deep_scan: args.deep_scan !== false,
1277
+ clean_threats: args.clean_threats === true,
1278
+ });
1279
+ result = await dispatchTaskToStudio("scan_security", args.target || "Workspace", payload, {
1280
+ placeId: args.placeId,
1281
+ studioId: args.studioId,
1282
+ });
1283
+ break;
1284
+ }
755
1285
  case "insert_asset": {
756
1286
  const assetId = String(args.asset_id ?? "");
757
1287
  if (!assetId)
758
1288
  throw new Error("Parameter 'asset_id' wajib diisi.");
759
- result = await dispatchTaskToStudio("insert_asset", args.target || "Workspace", assetId, {
1289
+ const payload = JSON.stringify({
1290
+ asset_id: assetId,
1291
+ strip_scripts: args.strip_scripts !== false,
1292
+ scan_security: args.scan_security !== false,
1293
+ });
1294
+ result = await dispatchTaskToStudio("insert_asset", args.target || "Workspace", payload, {
760
1295
  placeId: args.placeId,
761
1296
  studioId: args.studioId,
762
1297
  });
@@ -802,6 +1337,269 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
802
1337
  }
803
1338
  break;
804
1339
  }
1340
+ case "get_roblox_docs": {
1341
+ const query = String(args.query ?? "").trim();
1342
+ if (!query)
1343
+ throw new Error("Parameter 'query' wajib diisi.");
1344
+ const member = args.member ? String(args.member).trim() : undefined;
1345
+ const category = args.category ? String(args.category).trim() : "auto";
1346
+ result = await (0, roblox_docs_1.getRobloxDocs)(query, member, category);
1347
+ break;
1348
+ }
1349
+ case "batch_create_instances": {
1350
+ const payload = JSON.stringify({
1351
+ target: args.target,
1352
+ tree: args.tree,
1353
+ instances: args.instances,
1354
+ });
1355
+ result = await dispatchTaskToStudio("batch_create_instances", args.target || "Workspace", payload, {
1356
+ placeId: args.placeId,
1357
+ studioId: args.studioId,
1358
+ });
1359
+ break;
1360
+ }
1361
+ case "batch_set_properties": {
1362
+ if (!args.updates || !Array.isArray(args.updates))
1363
+ throw new Error("Parameter 'updates' wajib berupa array.");
1364
+ const payload = JSON.stringify({
1365
+ updates: args.updates,
1366
+ });
1367
+ result = await dispatchTaskToStudio("batch_set_properties", "", payload, {
1368
+ placeId: args.placeId,
1369
+ studioId: args.studioId,
1370
+ });
1371
+ break;
1372
+ }
1373
+ case "scan_vfx": {
1374
+ const target = String(args.target ?? "");
1375
+ if (!target)
1376
+ throw new Error("Parameter 'target' wajib diisi.");
1377
+ const payload = JSON.stringify({
1378
+ name: args.name,
1379
+ category: args.category,
1380
+ tags: args.tags,
1381
+ description: args.description,
1382
+ });
1383
+ const scanRes = await dispatchTaskToStudio("scan_vfx", target, payload, {
1384
+ placeId: args.placeId,
1385
+ studioId: args.studioId,
1386
+ });
1387
+ if (scanRes && (scanRes.status === "success" || !scanRes.error)) {
1388
+ const scanned = scanRes.result || scanRes;
1389
+ const db = (0, vfx_database_1.getVfxDatabase)();
1390
+ const saved = db.saveVfx({
1391
+ name: scanned.name || args.name || "Scanned_VFX",
1392
+ category: scanned.category || args.category || "Ambient",
1393
+ tags: scanned.tags || args.tags || [],
1394
+ description: scanned.description || args.description,
1395
+ sourcePlaceId: args.placeId,
1396
+ stats: scanned.stats,
1397
+ nodeCount: scanned.nodeCount,
1398
+ data: scanned.data,
1399
+ });
1400
+ result = {
1401
+ status: "success",
1402
+ message: `VFX '${saved.name}' berhasil dipindai dan disimpan ke database persisten MCP dengan ID '${saved.id}'`,
1403
+ item: saved,
1404
+ };
1405
+ }
1406
+ else {
1407
+ throw new Error(scanRes?.error || "Gagal memindai VFX dari Studio.");
1408
+ }
1409
+ break;
1410
+ }
1411
+ case "apply_vfx": {
1412
+ const vfxId = String(args.vfx_id ?? "");
1413
+ const target = String(args.target ?? "");
1414
+ if (!vfxId)
1415
+ throw new Error("Parameter 'vfx_id' wajib diisi.");
1416
+ if (!target)
1417
+ throw new Error("Parameter 'target' wajib diisi.");
1418
+ const db = (0, vfx_database_1.getVfxDatabase)();
1419
+ const item = db.getVfx(vfxId);
1420
+ if (!item)
1421
+ throw new Error(`VFX dengan ID '${vfxId}' tidak ditemukan di database MCP.`);
1422
+ const payload = JSON.stringify({
1423
+ data: item.data,
1424
+ scale: args.scale,
1425
+ enabled: args.enabled,
1426
+ });
1427
+ result = await dispatchTaskToStudio("apply_vfx", target, payload, {
1428
+ placeId: args.placeId,
1429
+ studioId: args.studioId,
1430
+ });
1431
+ break;
1432
+ }
1433
+ case "list_vfx_database": {
1434
+ const db = (0, vfx_database_1.getVfxDatabase)();
1435
+ const items = db.listVfx({ category: args.category, query: args.query });
1436
+ const categories = db.getCategories();
1437
+ result = {
1438
+ total: items.length,
1439
+ categories,
1440
+ items: items.map(i => ({
1441
+ id: i.id,
1442
+ name: i.name,
1443
+ category: i.category,
1444
+ tags: i.tags,
1445
+ description: i.description,
1446
+ nodeCount: i.nodeCount,
1447
+ stats: i.stats,
1448
+ createdAt: i.createdAt,
1449
+ })),
1450
+ };
1451
+ break;
1452
+ }
1453
+ case "delete_vfx": {
1454
+ const id = String(args.id ?? "");
1455
+ if (!id)
1456
+ throw new Error("Parameter 'id' wajib diisi.");
1457
+ const db = (0, vfx_database_1.getVfxDatabase)();
1458
+ const deleted = db.deleteVfx(id);
1459
+ if (!deleted)
1460
+ throw new Error(`VFX dengan ID '${id}' tidak ditemukan di database MCP.`);
1461
+ result = { status: "success", message: `VFX '${id}' berhasil dihapus dari database.` };
1462
+ break;
1463
+ }
1464
+ case "animate_character": {
1465
+ const target = String(args.target ?? "");
1466
+ const action = String(args.action ?? "");
1467
+ if (!target)
1468
+ throw new Error("Parameter 'target' wajib diisi.");
1469
+ if (!action)
1470
+ throw new Error("Parameter 'action' wajib diisi.");
1471
+ const payload = JSON.stringify({
1472
+ action,
1473
+ parameters: args.parameters || {},
1474
+ key_poses: args.key_poses,
1475
+ });
1476
+ result = await dispatchTaskToStudio("animate_character", target, payload, {
1477
+ placeId: args.placeId,
1478
+ studioId: args.studioId,
1479
+ });
1480
+ break;
1481
+ }
1482
+ case "solve_limb_ik": {
1483
+ const target = String(args.target ?? "");
1484
+ const limb = String(args.limb ?? "");
1485
+ if (!target)
1486
+ throw new Error("Parameter 'target' wajib diisi.");
1487
+ if (!limb)
1488
+ throw new Error("Parameter 'limb' wajib diisi.");
1489
+ if (!args.goal || !Array.isArray(args.goal) || args.goal.length < 3) {
1490
+ throw new Error("Parameter 'goal' [X, Y, Z] wajib diisi.");
1491
+ }
1492
+ const payload = JSON.stringify({
1493
+ limb,
1494
+ goal: args.goal,
1495
+ pole: args.pole,
1496
+ apply_to_rig: args.apply_to_rig !== false,
1497
+ });
1498
+ result = await dispatchTaskToStudio("solve_limb_ik", target, payload, {
1499
+ placeId: args.placeId,
1500
+ studioId: args.studioId,
1501
+ });
1502
+ break;
1503
+ }
1504
+ case "scrub_timeline": {
1505
+ const target = String(args.target ?? "");
1506
+ if (!target)
1507
+ throw new Error("Parameter 'target' wajib diisi.");
1508
+ const payload = JSON.stringify({
1509
+ time: args.time,
1510
+ action: args.action,
1511
+ emit_vfx: args.emit_vfx !== false,
1512
+ emit_count: args.emit_count,
1513
+ enabled: args.enabled,
1514
+ });
1515
+ result = await dispatchTaskToStudio("scrub_timeline", target, payload, {
1516
+ placeId: args.placeId,
1517
+ studioId: args.studioId,
1518
+ });
1519
+ break;
1520
+ }
1521
+ case "inspect_rig": {
1522
+ const target = String(args.target ?? "");
1523
+ if (!target)
1524
+ throw new Error("Parameter 'target' wajib diisi.");
1525
+ const payload = JSON.stringify({ action: "inspect" });
1526
+ result = await dispatchTaskToStudio("edit_rig", target, payload, {
1527
+ placeId: args.placeId,
1528
+ studioId: args.studioId,
1529
+ });
1530
+ break;
1531
+ }
1532
+ case "edit_rig_joint": {
1533
+ const target = String(args.target ?? "");
1534
+ const action = String(args.action ?? "edit_pivot");
1535
+ if (!target)
1536
+ throw new Error("Parameter 'target' wajib diisi.");
1537
+ const payload = JSON.stringify({
1538
+ action,
1539
+ joint_name: args.joint_name,
1540
+ part0: args.part0,
1541
+ part1: args.part1,
1542
+ delta_world: args.delta_world,
1543
+ target_world: args.target_world,
1544
+ rotate_euler_deg: args.rotate_euler_deg,
1545
+ align_to: args.align_to,
1546
+ reanchor: args.reanchor,
1547
+ });
1548
+ result = await dispatchTaskToStudio("edit_rig", target, payload, {
1549
+ placeId: args.placeId,
1550
+ studioId: args.studioId,
1551
+ });
1552
+ break;
1553
+ }
1554
+ case "auto_rig": {
1555
+ const target = String(args.target ?? "");
1556
+ const rigType = String(args.rig_type ?? "R15");
1557
+ if (!target)
1558
+ throw new Error("Parameter 'target' wajib diisi.");
1559
+ let action = "auto_rig_r15";
1560
+ if (rigType === "R6")
1561
+ action = "auto_rig_r6";
1562
+ else if (rigType === "Weapon")
1563
+ action = "rig_weapon";
1564
+ const payload = JSON.stringify({
1565
+ action,
1566
+ weapon_target: args.weapon_target,
1567
+ target_limb: args.target_limb,
1568
+ });
1569
+ result = await dispatchTaskToStudio("edit_rig", target, payload, {
1570
+ placeId: args.placeId,
1571
+ studioId: args.studioId,
1572
+ });
1573
+ break;
1574
+ }
1575
+ case "weld_assembly": {
1576
+ const target = String(args.target ?? "");
1577
+ if (!target)
1578
+ throw new Error("Parameter 'target' wajib diisi.");
1579
+ const payload = JSON.stringify({
1580
+ action: "weld_assembly",
1581
+ primary_part: args.primary_part,
1582
+ });
1583
+ result = await dispatchTaskToStudio("edit_rig", target, payload, {
1584
+ placeId: args.placeId,
1585
+ studioId: args.studioId,
1586
+ });
1587
+ break;
1588
+ }
1589
+ case "mirror_rig": {
1590
+ const target = String(args.target ?? "");
1591
+ if (!target)
1592
+ throw new Error("Parameter 'target' wajib diisi.");
1593
+ const payload = JSON.stringify({
1594
+ action: "mirror",
1595
+ source_side: args.source_side || "Left",
1596
+ });
1597
+ result = await dispatchTaskToStudio("edit_rig", target, payload, {
1598
+ placeId: args.placeId,
1599
+ studioId: args.studioId,
1600
+ });
1601
+ break;
1602
+ }
805
1603
  case "call_mcp_tool": {
806
1604
  const { command, target, data, placeId, studioId } = args;
807
1605
  if (!command)
@@ -815,12 +1613,52 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
815
1613
  if (target)
816
1614
  backupScript(target, String(data));
817
1615
  }
1616
+ // Intercept get_roblox_docs di Node.js
1617
+ if (command === "get_roblox_docs") {
1618
+ const queryParams = typeof data === "string" ? (function () { try {
1619
+ return JSON.parse(data);
1620
+ }
1621
+ catch {
1622
+ return { query: data };
1623
+ } })() : (data || {});
1624
+ const q = String(queryParams.query || target || "").trim();
1625
+ result = await (0, roblox_docs_1.getRobloxDocs)(q, queryParams.member, queryParams.category);
1626
+ break;
1627
+ }
818
1628
  // Intercept search_asset di Node.js
819
1629
  if (command === "search_asset" && data) {
820
1630
  const queryParams = typeof data === "string" ? JSON.parse(data) : data;
821
1631
  result = await handleSearchAsset(queryParams.keyword || "", queryParams.category);
822
1632
  break;
823
1633
  }
1634
+ // Intercept VFX database tools di Node.js
1635
+ if (command === "list_vfx_database") {
1636
+ const queryParams = typeof data === "string" ? JSON.parse(data || "{}") : (data || {});
1637
+ const db = (0, vfx_database_1.getVfxDatabase)();
1638
+ result = {
1639
+ total: db.listVfx(queryParams).length,
1640
+ categories: db.getCategories(),
1641
+ items: db.listVfx(queryParams),
1642
+ };
1643
+ break;
1644
+ }
1645
+ if (command === "delete_vfx") {
1646
+ const queryParams = typeof data === "string" ? JSON.parse(data || "{}") : (data || {});
1647
+ const db = (0, vfx_database_1.getVfxDatabase)();
1648
+ const ok = db.deleteVfx(queryParams.id || target || "");
1649
+ result = { success: ok };
1650
+ break;
1651
+ }
1652
+ if (command === "apply_vfx") {
1653
+ const queryParams = typeof data === "string" ? JSON.parse(data || "{}") : (data || {});
1654
+ const db = (0, vfx_database_1.getVfxDatabase)();
1655
+ const item = db.getVfx(queryParams.vfx_id || queryParams.id || "");
1656
+ if (!item)
1657
+ throw new Error("VFX tidak ditemukan di database MCP.");
1658
+ const payload = JSON.stringify({ data: item.data, scale: queryParams.scale, enabled: queryParams.enabled });
1659
+ result = await dispatchTaskToStudio("apply_vfx", target || queryParams.target || "Workspace", payload, { placeId, studioId });
1660
+ break;
1661
+ }
824
1662
  result = await dispatchTaskToStudio(command, target || "", data, { placeId, studioId });
825
1663
  break;
826
1664
  }