Skip to content

Conversation

@saqadri
Copy link
Collaborator

@saqadri saqadri commented Sep 14, 2025

This is consistent with other deployment platforms (Vercel, etc.), to tag the state of your codebase at the point in time that it was deployed. Also add a breadcrumbs file to the deployment so this information is tracked. In the (near) future we can include this information in deployment metadata in DB as well.

Summary by CodeRabbit

  • New Features

    • Optional --git-tag to create a local deploy tag.
    • Bundles now include git or workspace metadata and a breadcrumb; CLI prints friendly hints about deploy source.
    • Best-effort version check that warns if a newer mcp-agent is available.
  • Refactor

    • Deploy flow now attaches deployment metadata (git/workspace) and preserves prior retry behavior.
    • Config/secrets discovery improved to track deployed secrets.
  • Documentation

    • Many CLI help texts updated to accept App ID, server URL, or app name.

@saqadri saqadri requested a review from rholinshead September 14, 2025 14:21
@coderabbitai
Copy link

coderabbitai bot commented Sep 14, 2025

Caution

Review failed

The pull request is closed.

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds git utilities and workspace fingerprinting; optionally creates a local git tag during deploy; embeds git/workspace metadata into Wrangler bundles and into MCPAppClient.deploy_app payloads (with a retry fallback if metadata is rejected); updates get_config_files to return deployed secrets path; adds a non-fatal version check utility.

Changes

Cohort / File(s) Summary
Deploy CLI — main deploy flow
src/mcp_agent/cli/cloud/commands/deploy/main.py
New --git-tag/--no-git-tag option; fetches git metadata and optionally creates a local annotated git tag using sanitized app name and UTC timestamp; calls _deploy_with_retry as before. _perform_api_deployment now collects git metadata and passes deployment_metadata to MCPAppClient.deploy_app; if the API rejects the metadata the code retries once with deployment_metadata=None. get_config_files callsite updated to handle the new third return (deployed_secrets_file). Minor formatting tweaks.
Wrangler bundling / metadata embedding
src/mcp_agent/cli/cloud/commands/deploy/wrangler_wrapper.py
After copying project to temp dir, detects git vs workspace via get_git_metadata; sets MCP_DEPLOY_SOURCE to "git" or "workspace", adds MCP_DEPLOY_TIME_UTC; when git present adds commit/short/branch/dirty to meta_vars and prints info; otherwise computes a workspace fingerprint (compute_directory_fingerprint) and prints info. Writes mcp_deploy_breadcrumb.py with breadcrumb dict and JSON, and injects [vars] and MCP_DEPLOY_META JSON into wrangler.toml, then proceeds with existing Wrangler invocation.
MCP App API client
src/mcp_agent/cli/mcp_app/api_client.py
MCPAppClient.deploy_app signature extended to deployment_metadata: Optional[Dict[str, Any]] = None; request payload typed and includes deploymentMetadata when provided (deploy timeout unchanged).
Git utilities module
src/mcp_agent/cli/utils/git_utils.py
New module providing GitMetadata dataclass and helpers: get_git_metadata, create_git_tag, sanitize_git_ref_component, utc_iso_now, compute_directory_hash, and compute_directory_fingerprint. Utilities are safe outside a repo, perform quiet subprocess/git probing, directory hashing/fingerprinting, and sanitization for git ref components.
Config files API
src/mcp_agent/cli/cloud/commands/deploy/main.py (callsites)
get_config_files(config_dir: Path) signature changed to return (config_file, Optional[secrets_file], Optional[deployed_secrets_file]); callsites updated to use deployed_secrets_file to decide secrets processing.
Version check utility & CLI integration
src/mcp_agent/cli/utils/version_check.py, src/mcp_agent/cli/main.py, src/mcp_agent/cli/cloud/main.py
New maybe_warn_newer_version() that performs a guarded, non-fatal PyPI lookup (5s timeout) and prints info if a newer version exists. CLI entrypoints call it in a best-effort, exception-swallowed manner before/inside CLI run.
Minor UX/help/formatting updates
src/mcp_agent/cli/cloud/commands/*, src/mcp_agent/cli/secrets/processor.py, src/mcp_agent/cli/utils/retry.py
Many commands broaden help text to accept "App ID, server URL, or app name"; several CLIError raises and other calls rewrapped/added trailing commas (formatting only).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant CLI as deploy_config
  participant FS as get_config_files
  participant Git as git_utils
  participant Wrangler as wrangler_wrapper
  participant API as MCPAppClient

  User->>CLI: run deploy (maybe --git-tag)
  CLI->>FS: get_config_files(config_dir)
  FS-->>CLI: (config_file, secrets_file, deployed_secrets_file)

  alt git-tag enabled
    CLI->>Git: get_git_metadata(project_dir)
    Git-->>CLI: GitMetadata or None
    alt metadata present
      CLI->>Git: sanitize_git_ref_component(name) & create_git_tag(tag,message)
      Git-->>CLI: success / skipped
    else no repo
      CLI-->>User: info "not a git repo, skipping tag"
    end
  end

  CLI->>Wrangler: wrangle & bundle (tmp project)
  Wrangler->>Git: get_git_metadata(temp_project_dir)
  alt git
    Git-->>Wrangler: GitMetadata
    Wrangler-->>Wrangler: write breadcrumb + wrangler.toml vars (MCP_DEPLOY_META)
  else workspace
    Wrangler->>Git: compute_directory_fingerprint(...)
    Wrangler-->>Wrangler: write breadcrumb + wrangler.toml vars (MCP_DEPLOY_META)
  end

  CLI->>API: deploy_app(app_id, deployment_metadata)
  alt API rejects metadata
    API-->>CLI: error
    CLI->>API: deploy_app(app_id, deployment_metadata=None)  %% retry without metadata
  end
  API-->>CLI: deploy result
  CLI-->>User: print final status / URL
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • rholinshead
  • petersonbill64

Poem

A rabbit scurries, tag in paw,
I stamp the commit for what I saw.
Breadcrumbs tucked in bundle deep,
Git or hash—your secrets keep.
Hop, deploy — the carrots leap! 🥕🐇

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "Add git tag/commit breadcrumbs to deploy flow" is concise and accurately summarizes the PR’s primary change of adding git-based tagging and breadcrumb metadata to the deployment flow. It highlights the most important intent without listing implementation details and is clear for a teammate scanning history. The phrasing is specific and directly related to the changes described in the diff.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a34a451 and 538e211.

📒 Files selected for processing (14)
  • src/mcp_agent/cli/cloud/commands/app/status/main.py (3 hunks)
  • src/mcp_agent/cli/cloud/commands/deploy/main.py (9 hunks)
  • src/mcp_agent/cli/cloud/commands/servers/delete/main.py (1 hunks)
  • src/mcp_agent/cli/cloud/commands/servers/describe/main.py (1 hunks)
  • src/mcp_agent/cli/cloud/commands/utils.py (3 hunks)
  • src/mcp_agent/cli/cloud/commands/workflows/cancel/main.py (3 hunks)
  • src/mcp_agent/cli/cloud/commands/workflows/describe/main.py (3 hunks)
  • src/mcp_agent/cli/cloud/commands/workflows/list/main.py (3 hunks)
  • src/mcp_agent/cli/cloud/commands/workflows/resume/main.py (4 hunks)
  • src/mcp_agent/cli/cloud/commands/workflows/runs/main.py (2 hunks)
  • src/mcp_agent/cli/cloud/main.py (3 hunks)
  • src/mcp_agent/cli/main.py (3 hunks)
  • src/mcp_agent/cli/utils/git_utils.py (1 hunks)
  • src/mcp_agent/cli/utils/version_check.py (1 hunks)

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment on lines +131 to +132
if fname in ignore_names or (fname.startswith(".") and fname != ".env"):
continue
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current condition has a logic issue with .env files. If .env is included in ignore_names, it will be skipped despite the special case handling. To correctly prioritize the .env exception, consider restructuring the condition like this:

if fname == ".env":
    pass  # Always include .env files
elif fname in ignore_names or fname.startswith("."):
    continue

This ensures .env files are always processed regardless of whether they're in the ignore list.

Suggested change
if fname in ignore_names or (fname.startswith(".") and fname != ".env"):
continue
if fname == ".env":
pass # Always include .env files
elif fname in ignore_names or fname.startswith("."):
continue

Spotted by Diamond

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/mcp_agent/cli/cloud/commands/deploy/wrangler_wrapper.py (1)

163-171: .env is bundled into the deployment — high risk of secret leakage

The copy filter deliberately includes “.env”. That file commonly contains secrets and will be shipped (renamed with .mcpac.py). This is a security footgun.

Recommended: exclude .env by default and allow opt-in via a flag if truly needed.

-                if (name.startswith(".") and name not in {".env"}) or name in {
+                if name.startswith(".") or name in {
                     "logs",
                     "__pycache__",
                     "node_modules",
                     "venv",
                     MCP_SECRETS_FILENAME,
                 }:

Optionally, support an env/flag MCP_INCLUDE_DOTENV to re-include if required.

🧹 Nitpick comments (9)
src/mcp_agent/cli/utils/git_utils.py (3)

55-55: Handle detached HEAD: branch shows as "HEAD"

When detached, rev-parse returns "HEAD". Consider normalizing to None to avoid misleading breadcrumbs.

-    branch = _run_git(["rev-parse", "--abbrev-ref", "HEAD"], project_dir)
+    branch_raw = _run_git(["rev-parse", "--abbrev-ref", "HEAD"], project_dir)
+    branch = None if branch_raw in (None, "HEAD") else branch_raw

71-73: Prefer second-level precision for ISO timestamps

Microseconds add churn in env/vars; seconds are sufficient and cleaner for display.

-def utc_iso_now() -> str:
-    return datetime.now(timezone.utc).isoformat()
+def utc_iso_now() -> str:
+    return datetime.now(timezone.utc).replace(microsecond=0).isoformat()

30-35: Swallowing all git errors hides actionable diagnostics

Catching Exception and returning None makes debugging hard (e.g., missing git binary vs not a repo). Consider distinguishing FileNotFoundError and optionally surfacing stderr in verbose mode.

-def _run_git(args: list[str], cwd: Path) -> Optional[str]:
-    try:
-        out = subprocess.check_output(["git", *args], cwd=str(cwd))
-        return out.decode("utf-8", errors="replace").strip()
-    except Exception:
-        return None
+def _run_git(args: list[str], cwd: Path) -> Optional[str]:
+    try:
+        out = subprocess.check_output(["git", *args], cwd=str(cwd), stderr=subprocess.STDOUT)
+        return out.decode("utf-8", errors="replace").strip()
+    except FileNotFoundError:
+        # git not installed
+        return None
+    except subprocess.CalledProcessError:
+        # command failed (e.g., not a repo, no tag) — treat as absence
+        return None
src/mcp_agent/cli/mcp_app/api_client.py (1)

309-314: Signature change looks good; add brief param doc for deployment_metadata

Add a one-liner in Args to document the optional metadata payload.

     """Deploy an MCP App via the API.
 
     Args:
         app_id: The UUID of the app to deploy
+        deployment_metadata: Optional metadata dict (e.g., git breadcrumb) sent to the API
src/mcp_agent/cli/cloud/commands/deploy/wrangler_wrapper.py (3)

211-219: Expose git tag in [vars] for parity with breadcrumb

You already compute git_meta.tag and include it in the Python breadcrumb. Adding MCP_DEPLOY_GIT_TAG to wrangler vars makes it observable at runtime without importing the module.

         if git_meta:
             meta_vars.update(
                 {
                     "MCP_DEPLOY_GIT_COMMIT": git_meta.commit_sha,
                     "MCP_DEPLOY_GIT_SHORT": git_meta.short_sha,
                     "MCP_DEPLOY_GIT_BRANCH": git_meta.branch or "",
                     "MCP_DEPLOY_GIT_DIRTY": "true" if git_meta.dirty else "false",
+                    "MCP_DEPLOY_GIT_TAG": git_meta.tag or "",
                 }
             )

282-285: TOML embedding: prefer single-line string for JSON if it fits

Triple-quoted TOML strings are fine, but single-quoted basic strings avoid surprises with escapes. Not blocking.

-        vars_lines.append(f'MCP_DEPLOY_META = """{meta_json}"""')
+        vars_lines.append(f"MCP_DEPLOY_META = '{meta_json}'")

241-279: Breadcrumb file name may collide with user modules

Writing mcp_deploy_breadcrumb.py at project root could shadow user code. Consider a more unique name (e.g., _mcp_deploy_breadcrumb.py) or place it under a private package/subdir.

-        (temp_project_dir / "mcp_deploy_breadcrumb.py").write_text(breadcrumb_py)
+        (temp_project_dir / "_mcp_deploy_breadcrumb.py").write_text(breadcrumb_py)
src/mcp_agent/cli/cloud/commands/deploy/main.py (2)

254-276: Tag format: guard against pathological app names

Sanitization replaces non-alnum with “-”, but names like “---” or very long names can create unwieldy tags or collisions. Consider trimming duplicate dashes and enforcing a max length (e.g., 63 chars for the middle segment).

-                safe_name = "".join(
-                    c if c.isalnum() or c in "-_" else "-" for c in app_name
-                )
+                cleaned = "".join(c if c.isalnum() or c in "-_" else "-" for c in app_name)
+                safe_name = re.sub(r"-{2,}", "-", cleaned).strip("-")[:63]

(Import re at top.)


292-304: Metadata is git-only by design — align with wrangler’s workspace fingerprint?

If you want parity with the bundle when outside git, consider including the workspace fingerprint here too (same ignore set), guarded by a flag. Not blocking for this PR.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4ad03ab and 1ff5460.

📒 Files selected for processing (4)
  • src/mcp_agent/cli/cloud/commands/deploy/main.py (5 hunks)
  • src/mcp_agent/cli/cloud/commands/deploy/wrangler_wrapper.py (2 hunks)
  • src/mcp_agent/cli/mcp_app/api_client.py (2 hunks)
  • src/mcp_agent/cli/utils/git_utils.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/mcp_agent/cli/cloud/commands/deploy/wrangler_wrapper.py (2)
src/mcp_agent/cli/utils/ux.py (1)
  • print_info (32-49)
src/mcp_agent/cli/utils/git_utils.py (3)
  • get_git_metadata (38-68)
  • compute_directory_fingerprint (113-150)
  • utc_iso_now (71-72)
src/mcp_agent/cli/cloud/commands/deploy/main.py (4)
src/mcp_agent/cli/utils/git_utils.py (2)
  • get_git_metadata (38-68)
  • create_git_tag (153-167)
src/mcp_agent/cli/utils/ux.py (2)
  • print_success (52-63)
  • print_info (32-49)
src/mcp_agent/cli/core/utils.py (1)
  • run_async (12-27)
src/mcp_agent/cli/mcp_app/api_client.py (1)
  • deploy_app (309-345)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: checks / test
🔇 Additional comments (1)
src/mcp_agent/cli/mcp_app/api_client.py (1)

330-334: Payload gating is correct — callers verified

deploymentMetadata is only added when provided. Repo search found only the deploy_app definition and one caller at src/mcp_agent/cli/cloud/commands/deploy/main.py:306–308 (mcp_app_client.deploy_app(app_id=app_id, deployment_metadata=metadata)); no other callers detected.

Comment on lines +85 to +90
for dirpath, dirnames, filenames in os.walk(root):
# Filter dirnames in-place to prune traversal
dirnames[:] = [
d for d in dirnames if d not in ignore_names and not d.startswith(".")
]
for fname in sorted(filenames):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Make directory traversal deterministic to stabilize hashes across platforms

os.walk() doesn’t guarantee directory order; since you hash in traversal order, results can vary. Sort dirnames to ensure stable digests.

Apply this diff in both walkers:

-        dirnames[:] = [
-            d for d in dirnames if d not in ignore_names and not d.startswith(".")
-        ]
+        dirnames[:] = sorted(
+            d for d in dirnames if d not in ignore_names and not d.startswith(".")
+        )

Also applies to: 126-130

🤖 Prompt for AI Agents
In src/mcp_agent/cli/utils/git_utils.py around lines 85-90 and also for the
second walker at 126-130, the directory traversal order is non-deterministic
because os.walk does not guarantee dirnames ordering; after you filter dirnames
in-place, sort dirnames in-place (e.g., dirnames.sort()) so traversal and
hashing are deterministic across platforms, ensuring both walkers apply the same
filter-then-sort step.

* WIP starting point

* Reorder app creation and prompt to redeploy

* Update secrets processing

* Track skipped secrets

* Improve messaging with path

* Fix validation test

* Transform tests

* Clean up tests
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/mcp_agent/cli/secrets/processor.py (1)

82-88: Raise CLIError on read failures for consistent UX.

print_error + bare raise loses CLI context. Raise CLIError with the cause so upstream can render a concise message.

-    try:
-        with open(input_path, "r", encoding="utf-8") as f:
-            input_secrets_content = f.read()
-    except Exception as e:
-        print_error(f"Failed to read secrets file: {str(e)}")
-        raise
+    try:
+        with open(input_path, "r", encoding="utf-8") as f:
+            input_secrets_content = f.read()
+    except Exception as e:
+        raise CLIError(f"Failed to read secrets file: {str(e)}") from e
🧹 Nitpick comments (10)
src/mcp_agent/cli/core/utils.py (1)

12-27: run_async fallback will still raise under a running loop.

Calling loop.run_until_complete while the loop is already running (e.g., pytest-asyncio, Jupyter) raises "This event loop is already running". Either:

  • document that run_async must not be used inside a running loop; or
  • change the fallback to detect loop.is_running() and return a Task (requiring async callers to await), or use a proven pattern (e.g., anyio) to bridge contexts.
src/mcp_agent/cli/secrets/processor.py (1)

356-386: Minor UX: avoid embedding leading newlines in error/info messages.

Messages like f"\nError processing secret..." add blank lines; keep formatting in caller. Not blocking.

tests/cli/commands/test_deploy_command.py (1)

72-77: Optional: assert new flags are shown in help.

Include --git-tag and --send-metadata to keep the help test aligned with the CLI surface.

src/mcp_agent/cli/cloud/commands/deploy/main.py (7)

167-170: Fix grammar in user prompt.

Minor copy nit in the confirmation message.

-                        f"Do you want deploy an update to the existing app ID: {app_id}?"
+                        f"Do you want to deploy an update to the existing app ID: {app_id}?"

173-176: Cancellation should exit cleanly, not return a misleading app_id; also avoid print_error for non-exceptional flow.

Returning the existing app_id implies success to callers. Prefer a clean exit with code 0 and a neutral message. Using print_error logs exc_info=True, which adds a bogus stack trace.

-                        print_error(
-                            "Cancelling deployment. Please choose a different app name."
-                        )
-                        return app_id
+                        print_info(
+                            "Deployment canceled by user. Choose a different app name to proceed."
+                        )
+                        raise typer.Exit(code=0)

191-216: Non-interactive mode can silently use stale deployed secrets; auto-reprocess when input is newer.

If both secrets files exist and --non-interactive is set, you always reuse the deployed file, even if MCP_SECRETS_FILENAME was updated. This can ship stale secret handles.

                 if non_interactive:
-                    print_info(
-                        "--non-interactive specified, using existing deployed secrets file without changes."
-                    )
+                    # If the input secrets is newer than the deployed one, re-process without prompting.
+                    if secrets_file and deployed_secrets_file and secrets_file.stat().st_mtime > deployed_secrets_file.stat().st_mtime:
+                        print_info(
+                            "--non-interactive: input secrets newer than deployed; re-processing automatically."
+                        )
+                        deployed_secrets_file = None
+                    else:
+                        print_info(
+                            "--non-interactive specified, using existing deployed secrets file without changes."
+                        )

224-236: Prefer Path composition over string formatting for filesystem paths.

More robust and cross-platform safe.

-            secrets_transformed_path = Path(
-                f"{config_dir}/{MCP_DEPLOYED_SECRETS_FILENAME}"
-            )
+            secrets_transformed_path = config_dir / MCP_DEPLOYED_SECRETS_FILENAME

254-276: Git tag breadcrumbing looks solid; consider clearer failure messaging.

Flow is safe and guarded. If tag creation fails (e.g., already exists), current message conflates “not a repo” with other failures. Optional: differentiate duplicate-tag vs. other errors in git_utils.create_git_tag and surface a specific message.


291-305: Trim potentially sensitive metadata; avoid sending commit message by default.

Commit messages can contain PII or internal details. Since the goal is provenance, the SHA/branch/tag/dirty state should suffice.

                         metadata = {
                             "source": "git",
                             "commit": gm.commit_sha,
                             "short": gm.short_sha,
                             "branch": gm.branch,
                             "dirty": gm.dirty,
                             "tag": gm.tag,
-                            "message": gm.commit_message,
                         }

Optionally add a flag like --include-commit-message if needed later.


71-76: Track reintroduction of --dry-run behind a follow-up issue.

Nice to have for CI sanity checks and UX. Recommend opening an issue so we don’t lose it.

Do you want me to draft an issue with a proposed scope and acceptance criteria?

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1ff5460 and 94d6630.

📒 Files selected for processing (8)
  • src/mcp_agent/cli/cloud/commands/deploy/main.py (10 hunks)
  • src/mcp_agent/cli/core/utils.py (1 hunks)
  • src/mcp_agent/cli/secrets/processor.py (12 hunks)
  • src/mcp_agent/cli/utils/ux.py (3 hunks)
  • tests/cli/commands/test_cli_secrets.py (0 hunks)
  • tests/cli/commands/test_deploy_command.py (5 hunks)
  • tests/cli/secrets/test_developer_secret_validation.py (0 hunks)
  • tests/cli/secrets/test_secrets_transform.py (12 hunks)
💤 Files with no reviewable changes (2)
  • tests/cli/secrets/test_developer_secret_validation.py
  • tests/cli/commands/test_cli_secrets.py
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-22T18:59:49.368Z
Learnt from: CR
PR: lastmile-ai/mcp-agent#0
File: examples/usecases/reliable_conversation/CLAUDE.md:0-0
Timestamp: 2025-07-22T18:59:49.368Z
Learning: Applies to examples/usecases/reliable_conversation/examples/reliable_conversation/src/utils/config.py : Configuration values such as quality_threshold, max_refinement_attempts, consolidation_interval, and evaluator_model_provider must be loaded from mcp_agent.config.yaml.

Applied to files:

  • src/mcp_agent/cli/cloud/commands/deploy/main.py
🧬 Code graph analysis (6)
src/mcp_agent/cli/core/utils.py (2)
src/mcp_agent/config.py (1)
  • Settings (583-690)
src/mcp_agent/cli/mcp_app/api_client.py (1)
  • MCPApp (22-29)
tests/cli/commands/test_deploy_command.py (2)
src/mcp_agent/cli/cloud/commands/deploy/main.py (1)
  • deploy_config (43-332)
src/mcp_agent/cli/mcp_app/mock_client.py (2)
  • get_app_id_by_name (36-45)
  • create_app (86-124)
src/mcp_agent/cli/secrets/processor.py (6)
src/mcp_agent/cli/exceptions.py (1)
  • CLIError (4-9)
src/mcp_agent/cli/utils/ux.py (3)
  • print_info (32-49)
  • print_warning (66-77)
  • print_error (80-91)
src/mcp_agent/cli/secrets/api_client.py (2)
  • get_secret_value (72-98)
  • create_secret (15-70)
src/mcp_agent/cli/secrets/yaml_tags.py (2)
  • DeveloperSecret (30-33)
  • UserSecret (24-27)
src/mcp_agent/cli/secrets/mock_client.py (2)
  • get_secret_value (70-85)
  • create_secret (30-68)
src/mcp_agent/cli/core/constants.py (1)
  • SecretType (35-39)
tests/cli/secrets/test_secrets_transform.py (6)
src/mcp_agent/cli/secrets/mock_client.py (2)
  • create_secret (30-68)
  • get_secret_value (70-85)
src/mcp_agent/cli/secrets/api_client.py (2)
  • create_secret (15-70)
  • get_secret_value (72-98)
tests/cli/fixtures/mock_secrets_client.py (2)
  • create_secret (26-63)
  • get_secret_value (65-84)
src/mcp_agent/cli/core/constants.py (1)
  • SecretType (35-39)
src/mcp_agent/cli/secrets/processor.py (1)
  • transform_config_recursive (321-534)
src/mcp_agent/cli/secrets/yaml_tags.py (3)
  • UserSecret (24-27)
  • DeveloperSecret (30-33)
  • load_yaml_with_secrets (97-107)
src/mcp_agent/cli/cloud/commands/deploy/main.py (7)
src/mcp_agent/cli/utils/ux.py (4)
  • print_error (80-91)
  • print_info (32-49)
  • print_success (52-63)
  • print_deployment_header (185-208)
src/mcp_agent/cli/utils/git_utils.py (2)
  • get_git_metadata (38-68)
  • create_git_tag (153-167)
src/mcp_agent/cli/exceptions.py (1)
  • CLIError (4-9)
src/mcp_agent/cli/mcp_app/api_client.py (1)
  • deploy_app (309-345)
src/mcp_agent/cli/core/api_client.py (1)
  • UnauthenticatedError (9-12)
src/mcp_agent/cli/core/utils.py (1)
  • run_async (12-27)
src/mcp_agent/cli/secrets/processor.py (1)
  • process_config_secrets (41-152)
src/mcp_agent/cli/utils/ux.py (1)
src/mcp_agent/logging/logger.py (1)
  • info (271-279)
🔇 Additional comments (6)
src/mcp_agent/cli/core/utils.py (1)

30-32: Signature reformat only — looks good.

No behavior change; callers remain compatible.

tests/cli/secrets/test_secrets_transform.py (3)

56-83: Good coverage of raw→handle path.

Validates name/type/value kwarg usage and UUID_PREFIX. Looks solid.


196-211: Empty-value skip behavior verified — nice.

Matches transform’s skip-on-empty design.


334-451: Reuse flow assertions are thorough.

Covers reuse, exclusion, and new-secret creation; consider adding a case for list reuse when you implement list handling in get_validated_config_secrets.

src/mcp_agent/cli/cloud/commands/deploy/main.py (2)

134-147: Good validation and early client construction.

Clear checks for API URL/key and immediate client instantiation look solid.


335-364: get_config_files: clean and correct.

Good validation and explicit returns.

Comment on lines 224 to 231
async def get_validated_config_secrets(
input_config: Dict[str, Any],
existing_config: Dict[str, Any],
client: SecretsClient,
non_interactive: bool,
path: str = "",
) -> Dict[str, Any]:
"""Validate the secrets in the existing_config against the SecretsClient with current API key
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

List reuse unsupported in get_validated_config_secrets (drops valid handles in arrays).

Only dicts are validated/reused; lists are ignored, so existing handles inside arrays won’t be reused. Add list handling to preserve validated items by index.

 async def get_validated_config_secrets(
@@
-    for key, existing_value in existing_config.items():
+    for key, existing_value in existing_config.items():
         current_path = f"{path}.{key}" if path else key
@@
-        elif isinstance(existing_value, dict):
+        elif isinstance(existing_value, dict):
             # Always recursively process nested dictionaries
             input_dict = (
                 input_config.get(key, {})
                 if isinstance(input_config.get(key), dict)
                 else {}
             )
             nested_validated = await get_validated_config_secrets(
                 input_dict, existing_value, client, non_interactive, current_path
             )
 
             if nested_validated:
                 validated_config[key] = nested_validated
+        elif isinstance(existing_value, list):
+            # Validate lists by index
+            input_list = (
+                input_config.get(key, [])
+                if isinstance(input_config.get(key), list)
+                else []
+            )
+            validated_list: List[Any] = []
+            for i, ev in enumerate(existing_value):
+                item_path = f"{current_path}[{i}]"
+                # If list is shorter in input, default to exclude unless user opts in
+                if i >= len(input_list):
+                    if not non_interactive:
+                        keep = typer.confirm(
+                            f"Secret at '{item_path}' exists in transformed secrets but not in raw secrets. Keep it?",
+                            default=False,
+                        )
+                        if keep:
+                            validated_list.append(ev)
+                    continue
+                iv = input_list[i]
+                if isinstance(ev, str) and SECRET_ID_PATTERN.match(ev):
+                    try:
+                        secret_value = await client.get_secret_value(ev)
+                        if iv == secret_value:
+                            validated_list.append(ev)
+                        else:
+                            if non_interactive:
+                                print_warning(
+                                    f"Secret at '{item_path}' value mismatch; will reprocess."
+                                )
+                            else:
+                                reuse = typer.confirm(
+                                    f"Secret at '{item_path}' value mismatch. Reuse existing handle?",
+                                    default=False,
+                                )
+                                if reuse:
+                                    validated_list.append(ev)
+                    except Exception as e:
+                        raise CLIError(
+                            f"Failed to validate secret at '{item_path}' in transformed secrets file: {str(e)}"
+                        ) from e
+                elif isinstance(ev, dict) and isinstance(iv, dict):
+                    nested = await get_validated_config_secrets(
+                        iv, ev, client, non_interactive, item_path
+                    )
+                    if nested:
+                        validated_list.append(nested)
+                # else: ignore unsupported shapes
+            if validated_list:
+                validated_config[key] = validated_list

Also applies to: 246-318

🤖 Prompt for AI Agents
In src/mcp_agent/cli/secrets/processor.py around lines 224-231 (and similarly
246-318), the function only handles dicts and neglects lists so existing secret
handles inside arrays are dropped; update get_validated_config_secrets to detect
list values and process them by index: when encountering a list in input_config
or existing_config, allocate a new list of the same length, iterate over each
index, and for each element either recursively call the same validation logic
(to handle nested dicts/lists/primitives) or reuse the corresponding
existing_config element if it has already been validated and matches expected
type/handle; preserve order and indices, keep non_interactive semantics, and
ensure the returned structure contains lists with validated/reused entries
rather than discarding them.

Comment on lines 110 to 115
def print_secrets_summary(
dev_secrets: List[Dict[str, str]],
deployment_secrets: List[Dict[str, str]],
user_secrets: List[str],
env_loaded: List[str],
prompted: List[str],
reused_secrets: Optional[List[Dict[str, str]]] = None,
reused_secrets: Optional[List[Dict[str, str]]] = [],
skipped_secrets: Optional[List[str]] = [],
) -> None:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid mutable default args in print_secrets_summary.

Defaulting lists to [] makes state bleed across calls. Use None and normalize inside.

Apply this diff:

-def print_secrets_summary(
-    deployment_secrets: List[Dict[str, str]],
-    user_secrets: List[str],
-    reused_secrets: Optional[List[Dict[str, str]]] = [],
-    skipped_secrets: Optional[List[str]] = [],
-) -> None:
+def print_secrets_summary(
+    deployment_secrets: List[Dict[str, str]],
+    user_secrets: List[str],
+    reused_secrets: Optional[List[Dict[str, str]]] = None,
+    skipped_secrets: Optional[List[str]] = None,
+) -> None:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def print_secrets_summary(
dev_secrets: List[Dict[str, str]],
deployment_secrets: List[Dict[str, str]],
user_secrets: List[str],
env_loaded: List[str],
prompted: List[str],
reused_secrets: Optional[List[Dict[str, str]]] = None,
reused_secrets: Optional[List[Dict[str, str]]] = [],
skipped_secrets: Optional[List[str]] = [],
) -> None:
def print_secrets_summary(
deployment_secrets: List[Dict[str, str]],
user_secrets: List[str],
reused_secrets: Optional[List[Dict[str, str]]] = None,
skipped_secrets: Optional[List[str]] = None,
) -> None:

Comment on lines 130 to 136
# Create a set of reused/skipped secret paths for fast lookup
reused_paths = (
{secret["path"] for secret in reused_secrets} if reused_secrets else set()
)
skipped_paths = set(skipped_secrets) if skipped_secrets else set()

# Add developer secrets
for secret in dev_secrets:
for secret in deployment_secrets:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Fix counts to match what’s actually rendered (exclude reused/skipped).

Currently, new_deployment_count uses len(deployment_secrets), but the table filters out reused/skipped paths—counts may not match rows. Compute created_count from filtered items and log that.

-    reused_paths = (
-        {secret["path"] for secret in reused_secrets} if reused_secrets else set()
-    )
-    skipped_paths = set(skipped_secrets) if skipped_secrets else set()
+    reused_secrets = reused_secrets or []
+    skipped_secrets = skipped_secrets or []
+    reused_paths = {s["path"] for s in reused_secrets}
+    skipped_paths = set(skipped_secrets)
+    created_count = sum(
+        1
+        for s in deployment_secrets
+        if s.get("path") not in reused_paths and s.get("path") not in skipped_paths
+    )
@@
-    reused_count = len(reused_secrets)
-    new_deployment_count = len(deployment_secrets)
+    reused_count = len(reused_secrets)
+    new_deployment_count = created_count
@@
-        f"Processed {new_deployment_count} new deployment secrets, reused {reused_count} existing secrets, "
-        f"and identified {len(user_secrets)} user secrets. Skipped {len(skipped_secrets)} secrets due to errors."
+        f"Processed {new_deployment_count} new deployment secrets, reused {reused_count} existing secrets, "
+        f"and identified {len(user_secrets)} user secrets. Skipped {len(skipped_secrets)} secrets due to errors."
@@
-        f"[info]Summary:[/info] {new_deployment_count} new secrets created, {reused_count} existing secrets reused, {len(user_secrets)} user secrets identified, {len(skipped_secrets)} secrets skipped due to errors."
+        f"[info]Summary:[/info] {new_deployment_count} new secrets created, {reused_count} existing secrets reused, {len(user_secrets)} user secrets identified, {len(skipped_secrets)} secrets skipped due to errors."

Also applies to: 171-182

Comment on lines 172 to 199
mock_client = AsyncMock()
mock_client.get_app_id_by_name.return_value = None # No existing app

# Mock the app object returned by create_app
mock_app = MagicMock()
mock_app.appId = MOCK_APP_ID
mock_client.create_app.return_value = mock_app

with (
patch(
"mcp_agent.cli.cloud.commands.deploy.main.wrangler_deploy",
return_value=MOCK_APP_ID,
),
patch(
"mcp_agent.cli.cloud.commands.deploy.main.MCPAppClient",
return_value=mock_client,
),
):
# Run the deploy command
result = deploy_config(
ctx=MagicMock(),
app_name=MOCK_APP_NAME,
app_description="A test MCP Agent app",
config_dir=temp_path,
api_url="http://test.api/",
api_key="test-token",
non_interactive=True, # Set to True to avoid prompting
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

This test can hit the real Secrets API unless process_config_secrets is mocked.

Unlike test_deploy_command_basic, this path doesn’t stub secrets processing; deploy_config will run it and attempt network calls. Mock it to keep tests hermetic.

         with (
             patch(
                 "mcp_agent.cli.cloud.commands.deploy.main.wrangler_deploy",
                 return_value=MOCK_APP_ID,
             ),
             patch(
                 "mcp_agent.cli.cloud.commands.deploy.main.MCPAppClient",
                 return_value=mock_client,
             ),
+            patch(
+                "mcp_agent.cli.secrets.processor.process_config_secrets",
+                side_effect=(
+                    # write a minimal transformed file and return summary
+                    lambda *a, **kw: (
+                        (temp_path / MCP_DEPLOYED_SECRETS_FILENAME).write_text(
+                            "server:\\n  api_key: mcpac_sc_mocked\\n"
+                        )
+                        or {
+                            "deployment_secrets": [],
+                            "user_secrets": [],
+                            "reused_secrets": [],
+                            "skipped_secrets": [],
+                        }
+                    )
+                ),
+            ),
         ):

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In tests/cli/commands/test_deploy_command.py around lines 172-199, the test
calls deploy_config without stubbing out process_config_secrets so it may
perform real Secrets API calls; patch the process_config_secrets function used
by deploy_config in the same with(...) context to return a safe value (e.g.,
None or an appropriate mocked secret dict) and use AsyncMock if the function is
async, ensuring the patch path matches the module import used by deploy_config
so the test remains hermetic.

Copy link
Member

@rholinshead rholinshead left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, assuming a rebase cleans up the unrelated changes. We should either land & deploy www before we land this or just temporarily comment out passing the metadata in the request (assuming it will cause an error and prevent deploy right now)

"--non-interactive",
help="Fail if secrets require prompting, do not prompt.",
),
dry_run: bool = typer.Option(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this branch just in a weird rebase state? It's showing some changes that are already in main

payload: Dict[str, Any] = {"appId": app_id}
if deployment_metadata:
# Tentative field; include only when requested
payload["deploymentMetadata"] = deployment_metadata
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll need to wait for www side to land and get deployed before landing this

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noting that the server-side changes have been deployed so this is good to go

Comment on lines 264 to 267
# Sanitize app name for tag safety
safe_name = "".join(
c if c.isalnum() or c in "-_" else "-" for c in app_name
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Insufficient input sanitization: The app name sanitization only allows alphanumeric characters, hyphens, and underscores, but replaces all other characters with hyphens. This could create collisions (e.g., 'app@name' and 'app#name' both become 'app-name') and doesn't prevent potential issues with git tag naming rules. Git tags have specific restrictions (can't start with '.', can't contain certain sequences like '..', etc.). The sanitization should follow git tag naming conventions more strictly.

Suggested change
# Sanitize app name for tag safety
safe_name = "".join(
c if c.isalnum() or c in "-_" else "-" for c in app_name
)
# Sanitize app name for git tag safety
# Git tags cannot:
# - start with a period (.)
# - contain spaces, ~, ^, :, ?, *, [, \, or control characters
# - contain the sequence '..'
# - end with a slash (/) or .lock
safe_name = app_name.strip()
# Replace problematic characters with hyphens
safe_name = re.sub(r'[~^:?*\[\\\s]', '-', safe_name)
# Handle consecutive dots
safe_name = re.sub(r'\.{2,}', '-', safe_name)
# Remove leading periods
safe_name = re.sub(r'^\.+', '', safe_name)
# Remove trailing .lock or slashes
safe_name = re.sub(r'(/|\.lock)$', '', safe_name)
# Ensure we don't have empty string after sanitization
if not safe_name:
safe_name = "unnamed-app"

Spotted by Diamond

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
src/mcp_agent/cli/utils/git_utils.py (2)

90-92: Stabilize traversal order to make hashes deterministic across platforms.

Sort dirnames in-place in both walkers; os.walk doesn’t guarantee order. This avoids hash drift.

-        dirnames[:] = [
-            d for d in dirnames if d not in ignore_names and not d.startswith(".")
-        ]
+        dirnames[:] = sorted(
+            d for d in dirnames if d not in ignore_names and not d.startswith(".")
+        )

Apply the same change to the second walker.

Also applies to: 130-132


94-99: Clarify the .env exception by prioritizing it in the condition.

Current logic works but is harder to read; prefer explicit early allow for .env.

-            if fname in ignore_names or fname.startswith("."):
-                # Allow .env explicitly
-                if fname == ".env":
-                    pass
-                else:
-                    continue
+            if fname == ".env":
+                pass  # Always include .env
+            elif fname in ignore_names or fname.startswith("."):
+                continue
🧹 Nitpick comments (4)
src/mcp_agent/cli/utils/git_utils.py (3)

103-113: Stream file reads and encode read failures to avoid memory spikes and silent collisions.

Reading entire files can be expensive; and treating unreadable files as empty risks collisions. Stream in chunks and include a sentinel on failure.

-            rel = fpath.relative_to(root).as_posix()
-            try:
-                with open(fpath, "rb") as f:
-                    data = f.read()
-            except Exception:
-                data = b""
-            h.update(rel.encode("utf-8"))
-            h.update(b"\0")
-            h.update(data)
-            h.update(b"\n")
+            rel = fpath.relative_to(root).as_posix()
+            try:
+                h.update(rel.encode("utf-8"))
+                h.update(b"\0")
+                with open(fpath, "rb") as f:
+                    for chunk in iter(lambda: f.read(1024 * 1024), b""):
+                        h.update(chunk)
+                h.update(b"\n")
+            except Exception as e:
+                h.update(rel.encode("utf-8"))
+                h.update(b"\0")
+                h.update(b"<ERROR:READ_FAILED>")
+                h.update(str(type(e).__name__).encode("utf-8"))
+                h.update(b"\n")

141-152: Use nanosecond mtime for a more collision‑resistant fingerprint.

Seconds granularity can miss multiple changes within the same second.

-                st = fpath.stat()
-                size = st.st_size
-                mtime = int(st.st_mtime)
+                st = fpath.stat()
+                size = st.st_size
+                mtime_ns = getattr(st, "st_mtime_ns", int(st.st_mtime * 1_000_000_000))
             except Exception:
                 size = -1
-                mtime = 0
+                mtime_ns = 0
             h.update(rel.encode("utf-8"))
             h.update(b"\0")
             h.update(str(size).encode("utf-8"))
             h.update(b"\0")
-            h.update(str(mtime).encode("utf-8"))
+            h.update(str(mtime_ns).encode("utf-8"))

56-57: Normalize detached HEAD branch to None.

When in detached HEAD, this returns "HEAD". Consider mapping to None for cleaner metadata.

-        branch = _run_git(["rev-parse", "--abbrev-ref", "HEAD"], project_dir)
+        branch = _run_git(["rev-parse", "--abbrev-ref", "HEAD"], project_dir)
+        if branch == "HEAD":
+            branch = None
src/mcp_agent/cli/cloud/commands/deploy/main.py (1)

268-270: Avoid tag collisions within the same second.

Two rapid deploys can generate the same tag. Include microseconds to reduce collisions.

-                ts = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
+                ts = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S.%f")
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 16888fd and 9073383.

📒 Files selected for processing (2)
  • src/mcp_agent/cli/cloud/commands/deploy/main.py (9 hunks)
  • src/mcp_agent/cli/utils/git_utils.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/mcp_agent/cli/cloud/commands/deploy/main.py (3)
src/mcp_agent/cli/utils/git_utils.py (2)
  • get_git_metadata (38-71)
  • create_git_tag (156-170)
src/mcp_agent/cli/core/utils.py (1)
  • run_async (12-27)
src/mcp_agent/cli/mcp_app/api_client.py (1)
  • deploy_app (309-346)
🔇 Additional comments (2)
src/mcp_agent/cli/cloud/commands/deploy/main.py (2)

171-174: Fix minor grammar in prompt.

[ suggest_nitpick_refactor ]

-                        f"Do you want deploy an update to the existing app ID: {app_id}?"
+                        f"Do you want to deploy an update to the existing app ID {app_id}?"

358-373: Deployment metadata is not sent to the API (no-op today)

payload["deploymentMetadata"] is commented out in src/mcp_agent/cli/mcp_app/api_client.py (around line 334); main builds/passes metadata in src/mcp_agent/cli/cloud/commands/deploy/main.py (lines 358–373). Keep as-is for forward compatibility.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/mcp_agent/cli/cloud/commands/workflows/list/main.py (1)

77-81: Don’t swallow errors — re-raise as CLIError so the command fails properly.

Catching and only printing the error results in a successful exit code despite failure. Re-raise (wrapping) to ensure non‑zero exit and consistent UX.

Apply this diff:

-            except Exception as e:
-                print_error(
-                    f"Error listing workflows for server {server_id_or_url}: {str(e)}"
-                )
+            except Exception as e:
+                raise CLIError(
+                    f"Error listing workflows for server {server_id_or_url}: {e}",
+                    retriable=True,
+                ) from e
🧹 Nitpick comments (2)
src/mcp_agent/cli/cloud/commands/workflows/list/main.py (2)

63-75: DRY: compute workflows_data once.

Removes duplication and keeps JSON/YAML branches symmetrical.

Apply this diff:

-                if format == "json":
-                    workflows_data = [workflow.model_dump() for workflow in workflows]
-                    print(
-                        json.dumps({"workflows": workflows_data}, indent=2, default=str)
-                    )
-                elif format == "yaml":
-                    workflows_data = [workflow.model_dump() for workflow in workflows]
-                    print(
-                        yaml.dump(
-                            {"workflows": workflows_data}, default_flow_style=False
-                        )
-                    )
+                workflows_data = [workflow.model_dump() for workflow in workflows]
+                if format == "json":
+                    print(json.dumps({"workflows": workflows_data}, indent=2, default=str))
+                elif format == "yaml":
+                    print(yaml.dump({"workflows": workflows_data}, default_flow_style=False))
                 else:  # text format
                     print_workflows(workflows)

23-24: Avoid shadowing built-in name ‘format’.

Rename param to output_format to prevent confusion, keep CLI flag as --format.

Apply this diff:

-async def _list_workflows_async(server_id_or_url: str, format: str = "text") -> None:
+async def _list_workflows_async(server_id_or_url: str, output_format: str = "text") -> None:
@@
-                if format == "json":
+                if output_format == "json":
@@
-                elif format == "yaml":
+                elif output_format == "yaml":
@@
-    format: Optional[str] = typer.Option(
+    output_format: Optional[str] = typer.Option(
         "text", "--format", help="Output format (text|json|yaml)"
     ),
@@
-    validate_output_format(format)
-    run_async(_list_workflows_async(server_id_or_url, format))
+    validate_output_format(output_format)
+    run_async(_list_workflows_async(server_id_or_url, output_format))

Also applies to: 108-109

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9073383 and 178975b.

📒 Files selected for processing (10)
  • src/mcp_agent/cli/cloud/commands/app/status/main.py (2 hunks)
  • src/mcp_agent/cli/cloud/commands/auth/whoami/main.py (1 hunks)
  • src/mcp_agent/cli/cloud/commands/utils.py (3 hunks)
  • src/mcp_agent/cli/cloud/commands/workflows/cancel/main.py (1 hunks)
  • src/mcp_agent/cli/cloud/commands/workflows/describe/main.py (1 hunks)
  • src/mcp_agent/cli/cloud/commands/workflows/list/main.py (1 hunks)
  • src/mcp_agent/cli/cloud/commands/workflows/resume/main.py (1 hunks)
  • src/mcp_agent/cli/cloud/commands/workflows/runs/main.py (1 hunks)
  • src/mcp_agent/cli/secrets/processor.py (2 hunks)
  • src/mcp_agent/cli/utils/retry.py (6 hunks)
✅ Files skipped from review due to trivial changes (9)
  • src/mcp_agent/cli/cloud/commands/workflows/cancel/main.py
  • src/mcp_agent/cli/cloud/commands/auth/whoami/main.py
  • src/mcp_agent/cli/cloud/commands/workflows/runs/main.py
  • src/mcp_agent/cli/cloud/commands/app/status/main.py
  • src/mcp_agent/cli/secrets/processor.py
  • src/mcp_agent/cli/cloud/commands/workflows/describe/main.py
  • src/mcp_agent/cli/cloud/commands/utils.py
  • src/mcp_agent/cli/cloud/commands/workflows/resume/main.py
  • src/mcp_agent/cli/utils/retry.py
🧰 Additional context used
🧬 Code graph analysis (1)
src/mcp_agent/cli/cloud/commands/workflows/list/main.py (1)
src/mcp_agent/cli/exceptions.py (1)
  • CLIError (4-10)
🔇 Additional comments (1)
src/mcp_agent/cli/cloud/commands/workflows/list/main.py (1)

46-49: LGTM: formatting-only change preserves behavior.

Multiline CLIError call with retriable=False reads cleaner; semantics unchanged.

@saqadri saqadri merged commit 94216ea into main Sep 16, 2025
7 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants