1
1
use color_eyre:: eyre:: { eyre, Result , WrapErr } ;
2
+ use color_eyre:: Help ;
2
3
use roblox_install:: RobloxStudio ;
3
4
use serde_json:: { json, Value } ;
4
5
use std:: fs:: File ;
5
6
use std:: io:: BufReader ;
6
7
use std:: io:: Write ;
7
8
use std:: path:: Path ;
8
9
use std:: path:: PathBuf ;
10
+ use std:: vec;
9
11
use std:: { env, fs, io} ;
10
12
11
- const DISCLAIMER : & str = "Roblox Studio MCP is ready to go.
12
-
13
- Please restart Studio and Claude to apply the changes.
13
+ fn get_message ( successes : String ) -> String {
14
+ format ! ( "Roblox Studio MCP is ready to go.
15
+ Please restart Studio and MCP clients to apply the changes.
16
+
17
+ MCP Clients set up:
18
+ {successes}
14
19
15
20
Note: connecting a third-party LLM to Roblox Studio via an MCP server will share your data with that external service provider. Please review their privacy practices carefully before proceeding.
16
- To uninstall, delete the MCPStudioPlugin.rbxm from your Plugins directory." ;
21
+ To uninstall, delete the MCPStudioPlugin.rbxm from your Plugins directory." )
22
+ }
17
23
18
24
// returns OS dependant claude_desktop_config.json path
19
25
fn get_claude_config ( ) -> Result < PathBuf > {
@@ -35,6 +41,13 @@ fn get_claude_config() -> Result<PathBuf> {
35
41
Ok ( config_path)
36
42
}
37
43
44
+ fn get_cursor_config ( ) -> Result < PathBuf > {
45
+ let home_dir = env:: var_os ( "HOME" )
46
+ . or_else ( || env:: var_os ( "USERPROFILE" ) )
47
+ . unwrap ( ) ;
48
+ Ok ( Path :: new ( & home_dir) . join ( ".cursor" ) . join ( "mcp.json" ) )
49
+ }
50
+
38
51
#[ cfg( target_os = "macos" ) ]
39
52
fn get_exe_path ( ) -> Result < PathBuf > {
40
53
use core_foundation:: url:: CFURL ;
@@ -52,7 +65,46 @@ fn get_exe_path() -> io::Result<PathBuf> {
52
65
env:: current_exe ( )
53
66
}
54
67
55
- async fn install_internal ( ) -> Result < ( ) > {
68
+ pub fn install_to_config < ' a > (
69
+ config_path : Result < PathBuf > ,
70
+ exe_path : & Path ,
71
+ name : & ' a str ,
72
+ ) -> Result < & ' a str > {
73
+ let config_path = config_path?;
74
+ let mut config: serde_json:: Map < String , Value > = {
75
+ if !config_path. exists ( ) {
76
+ let mut file = File :: create ( & config_path) . map_err ( |e| {
77
+ eyre ! ( "Could not create {name} config file at {config_path:?}: {e:#?}" )
78
+ } ) ?;
79
+ file. write_all ( serde_json:: to_string ( & serde_json:: Map :: new ( ) ) ?. as_bytes ( ) ) ?;
80
+ }
81
+ let config_file = File :: open ( & config_path)
82
+ . map_err ( |error| eyre ! ( "Could not read or create {name} config file: {error:#?}" ) ) ?;
83
+ let reader = BufReader :: new ( config_file) ;
84
+ serde_json:: from_reader ( reader) ?
85
+ } ;
86
+
87
+ if !matches ! ( config. get( "mcpServers" ) , Some ( Value :: Object ( _) ) ) {
88
+ config. insert ( "mcpServers" . to_string ( ) , json ! ( { } ) ) ;
89
+ }
90
+
91
+ config[ "mcpServers" ] [ "Roblox Studio" ] = json ! ( {
92
+ "command" : & exe_path,
93
+ "args" : [
94
+ "--stdio"
95
+ ]
96
+ } ) ;
97
+
98
+ let mut file = File :: create ( & config_path) ?;
99
+ file. write_all ( serde_json:: to_string_pretty ( & config) ?. as_bytes ( ) )
100
+ . map_err ( |e| eyre ! ( "Could not write to {name} config file at {config_path:?}: {e:#?}" ) ) ?;
101
+
102
+ println ! ( "Installed MCP Studio plugin to {name} config {config_path:?}" ) ;
103
+
104
+ Ok ( name)
105
+ }
106
+
107
+ async fn install_internal ( ) -> Result < String > {
56
108
let plugin_bytes = include_bytes ! ( concat!( env!( "OUT_DIR" ) , "/MCPStudioPlugin.rbxm" ) ) ;
57
109
let studio = RobloxStudio :: locate ( ) ?;
58
110
let plugins = studio. plugins_path ( ) ;
@@ -76,43 +128,32 @@ async fn install_internal() -> Result<()> {
76
128
output_plugin. display( )
77
129
) ;
78
130
79
- let claude_config_path = get_claude_config ( ) ?;
80
-
81
- let mut config: serde_json:: Map < String , Value > = {
82
- if !claude_config_path. exists ( ) {
83
- let mut file = File :: create ( & claude_config_path) . map_err ( |e| {
84
- eyre ! ( "Could not create Claude config file at {claude_config_path:?}: {e:#?}" )
85
- } ) ?;
86
- file. write_all ( serde_json:: to_string ( & serde_json:: Map :: new ( ) ) ?. as_bytes ( ) ) ?;
87
- }
88
- let claude_config_file = File :: open ( & claude_config_path)
89
- . map_err ( |error| eyre ! ( "Could not read or create claude config file: {error:#?}" ) ) ?;
90
- let reader = BufReader :: new ( claude_config_file) ;
91
- serde_json:: from_reader ( reader) ?
92
- } ;
93
-
94
- if !matches ! ( config. get( "mcpServers" ) , Some ( Value :: Object ( _) ) ) {
95
- config. insert ( "mcpServers" . to_string ( ) , json ! ( { } ) ) ;
96
- }
97
-
98
131
let this_exe = get_exe_path ( ) ?;
99
- config[ "mcpServers" ] [ "Roblox Studio" ] = json ! ( {
100
- "command" : & this_exe,
101
- "args" : [
102
- "--stdio"
103
- ]
104
- } ) ;
105
132
106
- let mut file = File :: create ( & claude_config_path) ?;
107
- file. write_all ( serde_json:: to_string_pretty ( & config) ?. as_bytes ( ) )
108
- . map_err ( |e| {
109
- eyre ! ( "Could not write to Claude config file at {claude_config_path:?}: {e:#?}" )
110
- } ) ?;
133
+ let mut errors = vec ! [ ] ;
134
+ let claude_config_path = get_claude_config ( ) ;
135
+ let results = vec ! [
136
+ install_to_config( claude_config_path, & this_exe, "Claude" ) ,
137
+ install_to_config( get_cursor_config( ) , & this_exe, "Cursor" ) ,
138
+ ] ;
139
+
140
+ let successes: Vec < _ > = results
141
+ . into_iter ( )
142
+ . filter_map ( |r| r. map_err ( |e| errors. push ( e) ) . ok ( ) )
143
+ . collect ( ) ;
144
+
145
+ if successes. is_empty ( ) {
146
+ let error = errors. into_iter ( ) . fold (
147
+ eyre ! ( "Failed to install to either Claude or Cursor" ) ,
148
+ |report, e| report. note ( e) ,
149
+ ) ;
150
+ return Err ( error) ;
151
+ }
111
152
112
- println ! ( "Installed MCP Studio plugin to Claude config {claude_config_path:?}" ) ;
113
153
println ! ( ) ;
114
- println ! ( "{DISCLAIMER}" ) ;
115
- Ok ( ( ) )
154
+ let msg = get_message ( successes. join ( "\n " ) ) ;
155
+ println ! ( "{}" , msg) ;
156
+ Ok ( msg)
116
157
}
117
158
118
159
#[ cfg( target_os = "windows" ) ]
@@ -128,14 +169,13 @@ pub async fn install() -> Result<()> {
128
169
#[ cfg( target_os = "macos" ) ]
129
170
pub async fn install ( ) -> Result < ( ) > {
130
171
use native_dialog:: { DialogBuilder , MessageLevel } ;
131
- let alert_builder = if let Err ( e ) = install_internal ( ) . await {
132
- DialogBuilder :: message ( )
172
+ let alert_builder = match install_internal ( ) . await {
173
+ Err ( e ) => DialogBuilder :: message ( )
133
174
. set_level ( MessageLevel :: Error )
134
- . set_text ( format ! ( "Errors occurred: {:#}" , e) )
135
- } else {
136
- DialogBuilder :: message ( )
175
+ . set_text ( format ! ( "Errors occurred: {:#}" , e) ) ,
176
+ Ok ( msg) => DialogBuilder :: message ( )
137
177
. set_level ( MessageLevel :: Info )
138
- . set_text ( DISCLAIMER )
178
+ . set_text ( msg ) ,
139
179
} ;
140
180
let _ = alert_builder. set_title ( "Roblox Studio MCP" ) . alert ( ) . show ( ) ;
141
181
Ok ( ( ) )
0 commit comments