@@ -7,7 +7,6 @@ use eyre::{
77 bail,
88} ;
99use fig_os_shim:: Context ;
10- use futures:: future:: ok;
1110use tracing:: warn;
1211
1312use crate :: cli:: {
@@ -43,33 +42,37 @@ pub async fn execute_mcp(args: Mcp) -> Result<ExitCode> {
4342}
4443
4544pub async fn add_mcp_server ( ctx : & Context , args : McpAdd ) -> Result < ( ) > {
45+ let scope = args. scope . unwrap_or ( Scope :: Workspace ) ;
4646 let config_path = resolve_scope_profile ( ctx, args. scope , args. profile . as_ref ( ) ) ?;
4747 let mut config: McpServerConfig = serde_json:: from_str ( & ctx. fs ( ) . read_to_string ( & config_path) . await ?) ?;
48- let merged_env = if args. env . is_empty ( ) {
49- None
50- } else {
51- let mut map = HashMap :: new ( ) ;
52- for m in args. env {
53- map. extend ( m) ;
54- }
55- Some ( map)
56- } ;
57- let val: CustomToolConfig = serde_json:: from_value ( serde_json:: json!( {
48+ let merged_env = args. env . into_iter ( ) . flatten ( ) . collect :: < HashMap < _ , _ > > ( ) ;
49+
50+ let tool: CustomToolConfig = serde_json:: from_value ( serde_json:: json!( {
5851 "command" : args. command,
5952 "env" : merged_env,
6053 "timeout" : args. timeout,
6154 } ) ) ?;
62- config. mcp_servers . insert ( args. name , val ) ;
55+ config. mcp_servers . insert ( args. name . clone ( ) , tool ) ;
6356 config. save_to_file ( ctx, & config_path) . await ?;
6457
58+ println ! (
59+ "✓ Added MCP server '{}' to {scope}" ,
60+ args. name,
61+ scope = scope_display( & scope, & args. profile)
62+ ) ;
6563 Ok ( ( ) )
6664}
6765
6866pub async fn remove_mcp_server ( ctx : & Context , args : McpRemove ) -> Result < ( ) > {
67+ let scope = args. scope . unwrap_or ( Scope :: Workspace ) ;
6968 let config_path = resolve_scope_profile ( ctx, args. scope , args. profile . as_ref ( ) ) ?;
70- let mut config = McpServerConfig :: load_from_file ( ctx, & config_path) . await ?;
7169
72- let scope = args. scope . unwrap_or ( Scope :: Workspace ) ;
70+ if !ctx. fs ( ) . exists ( & config_path) {
71+ println ! ( "No MCP configuration at {}" , config_path. display( ) ) ;
72+ return Ok ( ( ) ) ;
73+ }
74+
75+ let mut config = McpServerConfig :: load_from_file ( ctx, & config_path) . await ?;
7376 match config. mcp_servers . remove ( & args. name ) {
7477 Some ( _) => {
7578 config. save_to_file ( ctx, & config_path) . await ?;
@@ -79,9 +82,7 @@ pub async fn remove_mcp_server(ctx: &Context, args: McpRemove) -> Result<()> {
7982 scope_display( & scope, & args. profile)
8083 ) ;
8184 } ,
82- None => {
83- warn ! ( ?args, "No MCP server found" ) ;
84- } ,
85+ None => println ! ( "No MCP server named '{}' found in {}" , args. name, scope_display( & scope, & args. profile) ) ,
8586 }
8687 Ok ( ( ) )
8788}
@@ -94,6 +95,7 @@ pub async fn list_mcp_server(ctx: &Context, args: McpList) -> Result<()> {
9495 }
9596
9697 for ( scope, profile, path, cfg_opt) in configs {
98+ println ! ( ) ;
9799 println ! ( "{}:" , scope_display( & scope, & profile) ) ;
98100 println ! ( " {}" , path. display( ) ) ;
99101 match cfg_opt {
@@ -103,13 +105,66 @@ pub async fn list_mcp_server(ctx: &Context, args: McpList) -> Result<()> {
103105 }
104106 } ,
105107 _ => {
106- println ! ( " null " ) ;
108+ println ! ( " (empty) " ) ;
107109 } ,
108110 }
109111 }
110112 Ok ( ( ) )
111113}
112114
115+ pub async fn import_mcp_server ( ctx : & Context , args : McpImport ) -> Result < ( ) > {
116+ let scope: Scope = args. scope . unwrap_or ( Scope :: Workspace ) ;
117+ let config_path = resolve_scope_profile ( ctx, args. scope , args. profile . as_ref ( ) ) ?;
118+ let mut dst_cfg: McpServerConfig = if ctx. fs ( ) . exists ( & config_path) {
119+ McpServerConfig :: load_from_file ( ctx, & config_path) . await ?
120+ } else {
121+ McpServerConfig :: default ( )
122+ } ;
123+ let src_path = expand_path ( ctx, & args. file ) ?;
124+ let src_cfg: McpServerConfig = serde_json:: from_str ( & ctx. fs ( ) . read_to_string ( & src_path) . await ?) ?;
125+
126+ let before = dst_cfg. mcp_servers . len ( ) ;
127+ for ( name, cfg) in src_cfg. mcp_servers {
128+ if dst_cfg. mcp_servers . insert ( name. clone ( ) , cfg) . is_some ( ) {
129+ warn ! ( %name, "Overwriting existing MCP server configuration" ) ;
130+ }
131+ }
132+ let added = dst_cfg. mcp_servers . len ( ) - before;
133+ dst_cfg. save_to_file ( ctx, & config_path) . await ?;
134+
135+ println ! (
136+ "✓ Imported {added} MCP server(s) into {:?} scope" ,
137+ scope_display( & scope, & args. profile)
138+ ) ;
139+ Ok ( ( ) )
140+ }
141+
142+ pub async fn get_mcp_server_status ( ctx : & Context , name : String ) -> Result < ( ) > {
143+ let configs = get_mcp_server_configs ( ctx, None , None ) . await ?;
144+
145+ for ( _, _, _, cfg_opt) in configs {
146+ if let Some ( cfg) = cfg_opt {
147+ if let Some ( tool_cfg) = cfg. mcp_servers . get ( & name) {
148+ println ! ( "MCP Server: {name}" ) ;
149+ println ! ( "Command : {}" , tool_cfg. command) ;
150+ println ! ( "Timeout : {} ms" , tool_cfg. timeout) ;
151+ println ! (
152+ "Env Vars : {}" ,
153+ tool_cfg
154+ . env
155+ . as_ref( )
156+ . map( |e| e. keys( ) . cloned( ) . collect:: <Vec <_>>( ) . join( ", " ) )
157+ . unwrap_or_else( || "(none)" . into( ) )
158+ ) ;
159+ // todo yifan how can I know the server status
160+ println ! ( "Status : " ) ;
161+ return Ok ( ( ) ) ;
162+ }
163+ }
164+ }
165+ bail ! ( "No MCP server named '{name}' found\n " )
166+ }
167+
113168async fn get_mcp_server_configs (
114169 ctx : & Context ,
115170 scope : Option < Scope > ,
@@ -160,70 +215,6 @@ fn scope_display(scope: &Scope, profile: &Option<String>) -> String {
160215 }
161216}
162217
163- pub async fn import_mcp_server ( ctx : & Context , args : McpImport ) -> Result < ( ) > {
164- let config_path = resolve_scope_profile ( ctx, args. scope , args. profile . as_ref ( ) ) ?;
165- let mut dst_cfg = if ctx. fs ( ) . exists ( & config_path) {
166- McpServerConfig :: load_from_file ( ctx, & config_path) . await ?
167- } else {
168- McpServerConfig :: default ( )
169- } ;
170- let expanded = shellexpand:: tilde ( & args. file ) ;
171- let mut src_path = std:: path:: PathBuf :: from ( expanded. as_ref ( ) ) ;
172- if src_path. is_relative ( ) {
173- src_path = ctx. env ( ) . current_dir ( ) ?. join ( src_path) ;
174- }
175-
176- let src_content = ctx
177- . fs ( )
178- . read_to_string ( & src_path)
179- . await
180- . map_err ( |e| eyre:: eyre!( "Failed to read source file '{}': {e}" , src_path. display( ) ) ) ?;
181- let src_cfg: McpServerConfig = serde_json:: from_str ( & src_content)
182- . map_err ( |e| eyre:: eyre!( "Invalid MCP JSON in '{}': {e}" , src_path. display( ) ) ) ?;
183-
184- let before = dst_cfg. mcp_servers . len ( ) ;
185- for ( name, cfg) in src_cfg. mcp_servers {
186- if dst_cfg. mcp_servers . insert ( name. clone ( ) , cfg) . is_some ( ) {
187- warn ! ( server = %name, "Overwriting existing MCP server configuration" ) ;
188- }
189- }
190- let added = dst_cfg. mcp_servers . len ( ) - before;
191-
192- dst_cfg. save_to_file ( ctx, & config_path) . await ?;
193-
194- let scope = args. scope . unwrap_or ( Scope :: Workspace ) ;
195- println ! (
196- "✓ Imported {added} MCP server(s) into {:?} scope" ,
197- scope_display( & scope, & args. profile)
198- ) ;
199- Ok ( ( ) )
200- }
201-
202- pub async fn get_mcp_server_status ( ctx : & Context , name : String ) -> Result < ( ) > {
203- let configs = get_mcp_server_configs ( ctx, None , None ) . await ?;
204- for ( _, _, _, cfg_opt) in configs {
205- if let Some ( cfg) = cfg_opt {
206- if let Some ( tool_cfg) = cfg. mcp_servers . get ( & name) {
207- println ! ( "MCP Server: {name}" ) ;
208- println ! ( "Command : {}" , tool_cfg. command) ;
209- println ! ( "Timeout : {} ms" , tool_cfg. timeout) ;
210- println ! (
211- "Env Vars : {}" ,
212- tool_cfg
213- . env
214- . as_ref( )
215- . map( |e| e. keys( ) . cloned( ) . collect:: <Vec <_>>( ) . join( ", " ) )
216- . unwrap_or_else( || "(none)" . into( ) )
217- ) ;
218- // todo yifan how can I know the server status
219- println ! ( "Status : " ) ;
220- return Ok ( ( ) ) ;
221- }
222- }
223- }
224- bail ! ( "No MCP server named '{name}' found\n " )
225- }
226-
227218fn resolve_scope_profile ( ctx : & Context , scope : Option < Scope > , profile : Option < & impl AsRef < str > > ) -> Result < PathBuf > {
228219 Ok ( match ( scope, profile) {
229220 ( None | Some ( Scope :: Workspace ) , _) => workspace_mcp_config_path ( ctx) ?,
@@ -233,6 +224,15 @@ fn resolve_scope_profile(ctx: &Context, scope: Option<Scope>, profile: Option<&i
233224 } )
234225}
235226
227+ fn expand_path ( ctx : & Context , p : & str ) -> Result < PathBuf > {
228+ let p = shellexpand:: tilde ( p) ;
229+ let mut path = PathBuf :: from ( p. as_ref ( ) ) ;
230+ if path. is_relative ( ) {
231+ path = ctx. env ( ) . current_dir ( ) ?. join ( path) ;
232+ }
233+ Ok ( path)
234+ }
235+
236236#[ cfg( test) ]
237237mod tests {
238238 use super :: * ;
0 commit comments