@@ -986,7 +986,17 @@ def _show_impl(mcp_id, output):
986986 rprint (f" { icon } { v ['stage' ]} : { v .get ('details' , '' ) or 'passed' } " )
987987
988988
989- def _install_impl (mcp_id , ide , raw , version = None ):
989+ def _install_impl (
990+ mcp_id ,
991+ ide ,
992+ raw ,
993+ version = None ,
994+ * ,
995+ env_overrides : dict [str , str ] | None = None ,
996+ header_overrides : dict [str , str ] | None = None ,
997+ env_file : str | None = None ,
998+ no_prompt : bool = False ,
999+ ):
9901000 optic .trace ("mcp_id={}, ide={}, version={}" , mcp_id , ide , version )
9911001 import json as _json
9921002
@@ -996,53 +1006,98 @@ def _install_impl(mcp_id, ide, raw, version=None):
9961006 with spinner ("Fetching server details..." ):
9971007 listing = client .get (f"/api/v1/mcps/{ resolved } " )
9981008
1009+ # Build env overrides from --env flags and --env-file
1010+ _env_from_flags : dict [str , str ] = dict (env_overrides ) if env_overrides else {}
1011+ if env_file :
1012+ for ev in _parse_env_file (env_file ):
1013+ if ev ["name" ] not in _env_from_flags :
1014+ _env_from_flags [ev ["name" ]] = ""
1015+ # Re-parse as key=value (env file has names only), read actual values from file
1016+ path = Path (env_file ).expanduser ().resolve ()
1017+ if path .exists ():
1018+ for line in path .read_text ().splitlines ():
1019+ line = line .strip ()
1020+ if not line or line .startswith ("#" ):
1021+ continue
1022+ if "=" in line :
1023+ k , _ , v = line .partition ("=" )
1024+ k = k .strip ()
1025+ v = v .strip ().strip ('"' ).strip ("'" )
1026+ if k :
1027+ _env_from_flags [k ] = v
1028+
1029+ _header_from_flags : dict [str , str ] = dict (header_overrides ) if header_overrides else {}
1030+ skip_prompts = raw or no_prompt
1031+
9991032 env_values : dict [str , str ] = {}
10001033 env_var_list = listing .get ("environment_variables" ) or []
1001- if env_var_list and not raw :
1034+ if env_var_list and not skip_prompts :
10021035 required = [ev for ev in env_var_list if ev .get ("required" , True )]
10031036 optional = [ev for ev in env_var_list if not ev .get ("required" , True )]
10041037
10051038 if required :
10061039 rprint (f"\n [bold]This server requires { len (required )} environment variable(s):[/bold]" )
10071040 for ev in required :
1008- desc = f" [dim]({ ev ['description' ]} )[/dim]" if ev .get ("description" ) else ""
1009- val = text_input (f" { ev ['name' ]} { desc } " )
1010- env_values [ev ["name" ]] = val
1041+ if ev ["name" ] in _env_from_flags :
1042+ env_values [ev ["name" ]] = _env_from_flags [ev ["name" ]]
1043+ rprint (f" [green]✓[/green] { ev ['name' ]} [dim](from --env)[/dim]" )
1044+ else :
1045+ desc = f" [dim]({ ev ['description' ]} )[/dim]" if ev .get ("description" ) else ""
1046+ val = text_input (f" { ev ['name' ]} { desc } " )
1047+ env_values [ev ["name" ]] = val
10111048
10121049 if optional :
10131050 rprint (f"\n [dim]{ len (optional )} optional env var(s) available:[/dim]" )
10141051 for ev in optional :
1015- desc = f" [dim]({ ev ['description' ]} )[/dim]" if ev .get ("description" ) else ""
1016- val = text_input (f" { ev ['name' ]} { desc } (press Enter to skip)" , default = "" )
1017- if val :
1018- env_values [ev ["name" ]] = val
1019- elif env_var_list and raw :
1020- # In raw mode, include placeholders so the user knows what's needed
1052+ if ev ["name" ] in _env_from_flags :
1053+ env_values [ev ["name" ]] = _env_from_flags [ev ["name" ]]
1054+ rprint (f" [green]✓[/green] { ev ['name' ]} [dim](from --env)[/dim]" )
1055+ else :
1056+ desc = f" [dim]({ ev ['description' ]} )[/dim]" if ev .get ("description" ) else ""
1057+ val = text_input (f" { ev ['name' ]} { desc } (press Enter to skip)" , default = "" )
1058+ if val :
1059+ env_values [ev ["name" ]] = val
1060+ elif env_var_list and skip_prompts :
1061+ # Non-interactive: use --env flag values, placeholders for the rest
10211062 for ev in env_var_list :
1022- env_values [ev ["name" ]] = f"<{ ev ['name' ]} >"
1063+ if ev ["name" ] in _env_from_flags :
1064+ env_values [ev ["name" ]] = _env_from_flags [ev ["name" ]]
1065+ else :
1066+ env_values [ev ["name" ]] = f"<{ ev ['name' ]} >"
10231067
10241068 # Prompt for headers (SSE/HTTP servers with auth)
10251069 header_values : dict [str , str ] = {}
10261070 header_list = listing .get ("headers" ) or []
1027- if header_list and not raw :
1071+ if header_list and not skip_prompts :
10281072 required_headers = [h for h in header_list if h .get ("required" , True )]
10291073 optional_headers = [h for h in header_list if not h .get ("required" , True )]
10301074 if required_headers :
10311075 rprint (f"\n [bold]This server requires { len (required_headers )} header(s):[/bold]" )
10321076 for h in required_headers :
1033- desc = f" [dim]({ h ['description' ]} )[/dim]" if h .get ("description" ) else ""
1034- val = text_input (f" { h ['name' ]} { desc } " )
1035- header_values [h ["name" ]] = val
1077+ if h ["name" ] in _header_from_flags :
1078+ header_values [h ["name" ]] = _header_from_flags [h ["name" ]]
1079+ rprint (f" [green]✓[/green] { h ['name' ]} [dim](from --header)[/dim]" )
1080+ else :
1081+ desc = f" [dim]({ h ['description' ]} )[/dim]" if h .get ("description" ) else ""
1082+ val = text_input (f" { h ['name' ]} { desc } " )
1083+ header_values [h ["name" ]] = val
10361084 if optional_headers :
10371085 rprint (f"\n [dim]{ len (optional_headers )} optional header(s) available:[/dim]" )
10381086 for h in optional_headers :
1039- desc = f" [dim]({ h ['description' ]} )[/dim]" if h .get ("description" ) else ""
1040- val = text_input (f" { h ['name' ]} { desc } (press Enter to skip)" , default = "" )
1041- if val :
1042- header_values [h ["name" ]] = val
1043- elif header_list and raw :
1087+ if h ["name" ] in _header_from_flags :
1088+ header_values [h ["name" ]] = _header_from_flags [h ["name" ]]
1089+ rprint (f" [green]✓[/green] { h ['name' ]} [dim](from --header)[/dim]" )
1090+ else :
1091+ desc = f" [dim]({ h ['description' ]} )[/dim]" if h .get ("description" ) else ""
1092+ val = text_input (f" { h ['name' ]} { desc } (press Enter to skip)" , default = "" )
1093+ if val :
1094+ header_values [h ["name" ]] = val
1095+ elif header_list and skip_prompts :
10441096 for h in header_list :
1045- header_values [h ["name" ]] = f"<{ h ['name' ]} >"
1097+ if h ["name" ] in _header_from_flags :
1098+ header_values [h ["name" ]] = _header_from_flags [h ["name" ]]
1099+ else :
1100+ header_values [h ["name" ]] = f"<{ h ['name' ]} >"
10461101
10471102 with spinner (f"Generating { ide } config..." ):
10481103 install_body = {"ide" : ide , "env_values" : env_values , "header_values" : header_values }
@@ -1302,31 +1357,64 @@ def install(
13021357 version : str | None = typer .Option (
13031358 None , "--version" , "-V" , help = "Install a specific version (e.g. '2.1.0'). Defaults to latest."
13041359 ),
1360+ env : list [str ] | None = typer .Option (None , "--env" , "-e" , help = "Environment variable (KEY=VALUE, repeatable)" ),
1361+ header : list [str ] | None = typer .Option (None , "--header" , help = "Header value (KEY=VALUE, repeatable)" ),
1362+ env_file : str | None = typer .Option (None , "--env-file" , help = "Path to .env file for environment variables" ),
1363+ no_prompt : bool = typer .Option (False , "--no-prompt" , "-y" , help = "Skip interactive prompts" ),
13051364):
13061365 """Generate an install config snippet for an MCP server.
13071366
13081367 Produces IDE-specific configuration that you paste into your editor's
13091368 MCP settings file. Prompts for required environment variables and
1310- headers interactively (unless --raw is used).
1369+ headers interactively (unless --raw or --no-prompt is used).
1370+
1371+ Use --env KEY=VALUE to pass environment variables non-interactively
1372+ (repeatable). Use --header KEY=VALUE for headers. Use --env-file to
1373+ load values from a .env file.
13111374
13121375 The --raw flag outputs bare JSON suitable for piping directly into
1313- config files, with placeholder values for env vars.
1376+ config files, with placeholder values for any missing env vars.
13141377
13151378 Examples:
13161379 # Generate config for Claude Code
13171380 observal registry mcp install my-server --ide claude-code
13181381
1382+ # Non-interactive with env vars
1383+ observal registry mcp install my-server --ide kiro --no-prompt --env API_KEY=sk-123
1384+
1385+ # Multiple env vars
1386+ observal registry mcp install my-server --ide cursor --env API_KEY=sk-123 --env SECRET=abc
1387+
1388+ # From env file
1389+ observal registry mcp install my-server --ide claude-code --env-file .env --no-prompt
1390+
13191391 # Generate for Cursor and pipe to config file
13201392 observal registry mcp install my-server --ide cursor --raw > .cursor/mcp.json
13211393
1322- # Install by row number for VS Code
1323- observal registry mcp install 2 --ide vscode
1324-
1325- # Install using an alias
1326- observal registry mcp install @db --ide kiro
1394+ # With headers for SSE servers
1395+ observal registry mcp install my-server --ide kiro --header Authorization='Bearer token'
13271396 """
13281397 optic .trace ("mcp_id={}, ide={}" , mcp_id , ide )
1329- _install_impl (mcp_id , ide , raw , version = version )
1398+ env_overrides = {}
1399+ for item in env or []:
1400+ k , _ , v = item .partition ("=" )
1401+ if k :
1402+ env_overrides [k .strip ()] = v
1403+ header_overrides = {}
1404+ for item in header or []:
1405+ k , _ , v = item .partition ("=" )
1406+ if k :
1407+ header_overrides [k .strip ()] = v
1408+ _install_impl (
1409+ mcp_id ,
1410+ ide ,
1411+ raw ,
1412+ version = version ,
1413+ env_overrides = env_overrides or None ,
1414+ header_overrides = header_overrides or None ,
1415+ env_file = env_file ,
1416+ no_prompt = no_prompt ,
1417+ )
13301418
13311419
13321420@mcp_app .command (name = "edit" )
@@ -1340,6 +1428,8 @@ def edit_mcp(
13401428 git_url : str | None = typer .Option (None , "--git-url" , help = "New git URL" ),
13411429 command : str | None = typer .Option (None , "--command" , help = "New command" ),
13421430 url : str | None = typer .Option (None , "--url" , help = "New URL" ),
1431+ bump : str | None = typer .Option (None , "--bump" , help = "Version bump type: patch, minor, or major (skips prompt)" ),
1432+ changelog : str | None = typer .Option (None , "--changelog" , help = "Changelog text for new version (skips prompt)" ),
13431433):
13441434 """Edit an MCP server submission.
13451435
@@ -1476,7 +1566,10 @@ def edit_mcp(
14761566 # Approved listing → publish a new version with semver bump
14771567 current_ver = listing .get ("version" , "0.1.0" ) if listing else "0.1.0"
14781568 rprint (f"[dim]Current version: { current_ver } [/dim]" )
1479- bump_type = select_one ("Version bump" , ["patch" , "minor" , "major" ], default = "patch" )
1569+ if bump and bump in ("patch" , "minor" , "major" ):
1570+ bump_type = bump
1571+ else :
1572+ bump_type = select_one ("Version bump" , ["patch" , "minor" , "major" ], default = "patch" )
14801573
14811574 parts = current_ver .split ("." )
14821575 if len (parts ) == 3 and all (p .isdigit () for p in parts ):
@@ -1491,7 +1584,7 @@ def edit_mcp(
14911584 _new_version = "0.2.0"
14921585
14931586 rprint (f"[bold]New version:[/bold] { _new_version } " )
1494- _changelog = text_input ("Changelog (what changed?)" , default = "" )
1587+ _changelog = changelog if changelog is not None else text_input ("Changelog (what changed?)" , default = "" )
14951588
14961589 # Separate top-level fields from extra (version-specific) fields
14971590 version_description = updates .pop ("description" , None ) or (listing .get ("description" , "" ) if listing else "" )
0 commit comments