Skip to content

Commit 253928f

Browse files
ShaanNarendranHaz3-jolt
authored andcommitted
feat(cli): add non-interactive flags for skill automation
Add --env, --header, --env-file, and --no-prompt flags to mcp install so the Observal skill can pass environment variables without interactive prompts. Add --env flag to agent pull and wire --no-prompt to skip env var collection. Add --bump and --changelog flags to mcp edit and --bump to agent publish to bypass interactive version bump selection. Commands affected: - observal registry mcp install: --env KEY=VALUE, --header KEY=VALUE, --env-file PATH, --no-prompt - observal agent pull: --env KEY=VALUE (works with existing --no-prompt) - observal registry mcp edit: --bump patch|minor|major, --changelog TEXT - observal agent publish: --bump patch|minor|major
1 parent 6ddcc78 commit 253928f

3 files changed

Lines changed: 183 additions & 50 deletions

File tree

observal_cli/cmd_agent.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -919,6 +919,7 @@ def agent_publish(
919919
update: bool = typer.Option(False, "--update", "-u", help="Update existing agent instead of creating"),
920920
draft: bool = typer.Option(False, "--draft", help="Save as draft instead of submitting for review"),
921921
submit: str | None = typer.Option(None, "--submit", help="Submit a draft agent for review (agent ID)"),
922+
bump: str | None = typer.Option(None, "--bump", help="Version bump type: patch, minor, or major (skips prompt)"),
922923
):
923924
"""Publish the agent definition to the server.
924925
@@ -975,10 +976,13 @@ def agent_publish(
975976
raise typer.Exit(code=1)
976977
agent_id = match["id"]
977978

978-
# Version bump selection (interactive only)
979+
# Version bump selection (interactive only when --bump not provided)
979980
import sys
980981

981-
if sys.stdin.isatty():
982+
if bump and bump in ("patch", "minor", "major"):
983+
payload["version_bump_type"] = bump
984+
payload.pop("version", None)
985+
elif sys.stdin.isatty():
982986
current_version = match.get("version", "1.0.0")
983987
try:
984988
suggestions = client.get(f"/api/v1/agents/{agent_id}/version-suggestions")

observal_cli/cmd_mcp.py

Lines changed: 125 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -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 "")

observal_cli/cmd_pull.py

Lines changed: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -111,13 +111,20 @@ def _resolve_hook_paths(content: str) -> str:
111111
return content
112112

113113

114-
def _collect_mcp_env_vars(agent_detail: dict) -> dict[str, dict[str, str]]:
114+
def _collect_mcp_env_vars(
115+
agent_detail: dict, *, no_prompt: bool = False, env_overrides: dict[str, str] | None = None
116+
) -> dict[str, dict[str, str]]:
115117
"""Discover MCP env vars from agent components and prompt the user for values.
116118
119+
When *no_prompt* is True, uses values from *env_overrides* for known vars
120+
and skips prompting entirely. Missing vars are omitted (server handles
121+
placeholders).
122+
117123
Returns {mcp_listing_id: {VAR_NAME: value}} for all MCPs that have env vars.
118124
"""
119125
optic.trace("agent_detail={}", agent_detail)
120126
env_values: dict[str, dict[str, str]] = {}
127+
_overrides = env_overrides or {}
121128

122129
# Collect MCP component IDs from both mcp_links and component_links
123130
mcp_ids: list[tuple[str, str]] = [] # (listing_id, display_name)
@@ -149,20 +156,34 @@ def _collect_mcp_env_vars(agent_detail: dict) -> dict[str, dict[str, str]]:
149156
mcp_name = display_name or listing.get("name", listing_id[:8])
150157
mcp_env: dict[str, str] = {}
151158

152-
if required:
153-
rprint(f"\n[bold]{mcp_name}[/bold] requires {len(required)} environment variable(s):")
154-
for ev in required:
155-
desc = f" [dim]({ev['description']})[/dim]" if ev.get("description") else ""
156-
val = text_input(f" {ev['name']}{desc}")
157-
mcp_env[ev["name"]] = val
158-
159-
if optional:
160-
rprint(f"\n[dim]{mcp_name}: {len(optional)} optional env var(s):[/dim]")
161-
for ev in optional:
162-
desc = f" [dim]({ev['description']})[/dim]" if ev.get("description") else ""
163-
val = text_input(f" {ev['name']}{desc} (press Enter to skip)", default="")
164-
if val:
165-
mcp_env[ev["name"]] = val
159+
if no_prompt:
160+
# Non-interactive: use --env flag values for matching vars
161+
for ev in required + optional:
162+
if ev["name"] in _overrides:
163+
mcp_env[ev["name"]] = _overrides[ev["name"]]
164+
else:
165+
if required:
166+
rprint(f"\n[bold]{mcp_name}[/bold] requires {len(required)} environment variable(s):")
167+
for ev in required:
168+
if ev["name"] in _overrides:
169+
mcp_env[ev["name"]] = _overrides[ev["name"]]
170+
rprint(f" [green]\u2713[/green] {ev['name']} [dim](from --env)[/dim]")
171+
else:
172+
desc = f" [dim]({ev['description']})[/dim]" if ev.get("description") else ""
173+
val = text_input(f" {ev['name']}{desc}")
174+
mcp_env[ev["name"]] = val
175+
176+
if optional:
177+
rprint(f"\n[dim]{mcp_name}: {len(optional)} optional env var(s):[/dim]")
178+
for ev in optional:
179+
if ev["name"] in _overrides:
180+
mcp_env[ev["name"]] = _overrides[ev["name"]]
181+
rprint(f" [green]\u2713[/green] {ev['name']} [dim](from --env)[/dim]")
182+
else:
183+
desc = f" [dim]({ev['description']})[/dim]" if ev.get("description") else ""
184+
val = text_input(f" {ev['name']}{desc} (press Enter to skip)", default="")
185+
if val:
186+
mcp_env[ev["name"]] = val
166187

167188
if mcp_env:
168189
env_values[listing_id] = mcp_env
@@ -453,6 +474,9 @@ def pull(
453474
False, "--refresh-models", help="Bust the local model catalog cache before showing the model picker"
454475
),
455476
no_prompt: bool = typer.Option(False, "--no-prompt", "-y", help="Skip interactive prompts"),
477+
env: list[str] | None = typer.Option(
478+
None, "--env", "-e", help="MCP environment variable (KEY=VALUE, repeatable)"
479+
),
456480
version: str | None = typer.Option(
457481
None, "--version", "-V", help="Install a specific version (e.g. '1.2.0'). Defaults to latest."
458482
),
@@ -463,20 +487,32 @@ def pull(
463487
then writes rules files, MCP configs, and agent files into the target
464488
directory. Use --dry-run to preview without writing.
465489
490+
Use --env KEY=VALUE to pass MCP environment variables non-interactively
491+
(repeatable). When --no-prompt is set, env var prompts are skipped and
492+
only values from --env flags are used.
493+
466494
Examples:
467495
observal agent pull my-agent --ide claude-code --no-prompt
468496
observal agent pull my-agent --ide claude-code --version 1.2.0
469497
observal agent pull my-agent --ide kiro --no-prompt --scope user
470498
observal agent pull my-agent --ide cursor --no-prompt --dry-run
499+
observal agent pull my-agent --ide kiro --no-prompt --env API_KEY=sk-123 --env SECRET=abc
471500
"""
472501
resolved = config.resolve_alias(agent_id)
473502
target_dir = Path(directory).resolve()
474503

504+
# Parse --env flags into overrides dict
505+
env_overrides: dict[str, str] = {}
506+
for item in env or []:
507+
k, _, v = item.partition("=")
508+
if k:
509+
env_overrides[k.strip()] = v
510+
475511
# Fetch agent details to discover MCP env vars
476512
with spinner("Fetching agent details..."):
477513
agent_detail = client.get(f"/api/v1/agents/{resolved}")
478514

479-
env_values = _collect_mcp_env_vars(agent_detail)
515+
env_values = _collect_mcp_env_vars(agent_detail, no_prompt=no_prompt, env_overrides=env_overrides or None)
480516

481517
rprint(f"\n[bold]Install options for [cyan]{ide}[/cyan]:[/bold]")
482518
if refresh_models:

0 commit comments

Comments
 (0)