Skip to content

Conversation

@saqadri
Copy link
Collaborator

@saqadri saqadri commented Oct 31, 2025

Summary

TLDR -- tries to implement these ergonomics:
image

Declaring environment variables meant editing raw YAML, secrets were processed server-side, and there was no convenient CLI to inspect or rotate env secrets after initial deploys. This PR brings the developer experience closer to Vercel’s env workflow by:

  • letting projects declare deployment env vars directly in mcp_agent.config.yaml
  • materializing those envs into secrets during cloud deploy, alongside a frozen config snapshot (mcp_agent.deployed.config.yaml)
  • exposing mcp-agent cloud secrets commands (list/add/remove/pull) so teams can manage env secrets without redeploying

Functional Changes

Config schema & runtime

  • Settings.env now accepts a list of strings or single-key dicts, e.g.:

    env:
      - OPENAI_API_KEY                         # resolved via os.environ
      - {SUPABASE_URL: "https://db.example"}    # fallback literal
  • Added validation helpers plus iter_env_specs() to preserve ordering and union string/dict entries.

  • MCPApp injects literal fallbacks into os.environ if the process is missing the variable; handles (mcpac_sc_*) are ignored so the control plane can still supply real values.

Deployment workflow

  • New materialize_deployment_artifacts() helper invoked from cloud deploy:
    • writes mcp_agent.deployed.config.yaml (user config snapshot)
    • reads each env spec, resolves values (os.environ → fallback literal), creates/updates secrets (apps/<app_id>/env/<KEY>), and writes their handles under env: in mcp_agent.deployed.secrets.yaml.
    • existing handles are reused to keep deployments idempotent.
  • Deploy logs now call out the new artefacts.

CLI enhancements

  • mcp-agent cloud secrets command group:
    • list [app] – show env secrets with masked handles.
    • add <key> <value> [app] – create or update a secret value.
    • remove <key> [app] – delete the secret handle.
    • pull [app] --output mcp_agent.cloud.secrets.yaml – download resolved values without touching mcp_agent.secrets.yaml.
  • Commands accept --config-dir, --api-url, and --api-key and fall back to project defaults when possible.

Documentation

  • Updated CLI reference, cloud quickstart, and manage-secrets guides with:
    • env declaration examples and how they feed into mcp_agent.deployed.secrets.yaml.
    • the presence and purpose of mcp_agent.deployed.config.yaml.
    • detailed walkthrough for the new cloud secrets subcommands.
    • callouts mirroring Vercel’s env pull experience.

Backend coordination

  • Bundle processor can now consume the pre-materialized config (BUNDLE_CONFIG_FILENAME) and still applies cloud overrides (Temporal, SSE). It skips gracefully if the file is missing or invalid.

Usage Walkthrough

uvx mcp-agent init --template basic cloud-demo
cd cloud-demo
export OPENAI_API_KEY="sk-..."               # values resolved at deploy time
export SUPABASE_URL="https://db.example.com"

# Declare env vars in mcp_agent.config.yaml:
# env:
#   - OPENAI_API_KEY
#   - {SUPABASE_URL: "https://db.example.com"}  # optional fallback

uvx mcp-agent deploy cloud-demo --non-interactive

uvx mcp-agent cloud secrets list cloud-demo
uvx mcp-agent cloud secrets add EXTRA_TOKEN "super-secret" cloud-demo
uvx mcp-agent cloud secrets pull cloud-demo --output mcp_agent.cloud.secrets.yaml
uvx mcp-agent cloud secrets remove EXTRA_TOKEN cloud-demo

mcp_agent.deployed.secrets.yaml now contains:

env:
  - OPENAI_API_KEY: mcpac_sc_...
  - SUPABASE_URL: mcpac_sc_...

And mcp_agent.deployed.config.yaml mirrors the project config after overrides, letting the control plane skip redundant transformations.

Summary by CodeRabbit

  • New Features

    • Declare env vars in config to capture them at deploy; deployment materializes config and secrets files for reproducible bundles
    • New cloud secrets commands: list, add, remove, pull for post-deploy inspection and rotation
  • Documentation

    • Updated deployment quickstart and CLI reference with env-var capture, materialized artifacts, and secrets management workflows
  • Tests

    • Added tests covering env spec parsing and deployment artifact materialization (including secret handle reuse and fallbacks)

@saqadri saqadri requested a review from rholinshead October 31, 2025 00:42
@coderabbitai
Copy link

coderabbitai bot commented Oct 31, 2025

Walkthrough

This PR adds environment-variable capture and management to cloud deployments: a new Settings.env field and validation, materialization of deployment artifacts (mcp_agent.deployed.secrets.yaml and mcp_agent.deployed.config.yaml) via a SecretsClient during deploy, post-deploy CLI secrets commands (list/add/remove/pull), and runtime population of os.environ from declared env specs.

Changes

Cohort / File(s) Summary
Docs
docs/cloud/deployment-quickstart.mdx, docs/cloud/mcp-agent-cloud/manage-secrets.mdx, docs/reference/cli.mdx
Documented API key placement, introduced deployed artifacts (mcp_agent.deployed.secrets.yaml, mcp_agent.deployed.config.yaml), described env capture in mcp_agent.config.yaml and mapping to deployed files, and added CLI secrets commands and usage.
Config model
src/mcp_agent/config.py
Added env field to Settings (list of strings or single-key dicts), validator for entries, and iter_env_specs() to iterate normalized env specs.
Runtime binding
src/mcp_agent/app.py
Added MCPApp._apply_environment_bindings and call during initialize() to populate os.environ from env specs (skips secret placeholders).
Deploy flow (materialization)
src/mcp_agent/cli/cloud/commands/deploy/materialize.py, src/mcp_agent/cli/cloud/commands/deploy/main.py
New materialize module with EnvSpec and materialize_deployment_artifacts to resolve env specs, create/update secrets via SecretsClient, and persist deployed config/secrets files. Integrated materialization into deploy flow and replaced inline config-default helper with get_app_defaults_from_config.
Secrets CLI
src/mcp_agent/cli/cloud/commands/secrets/__init__.py, src/mcp_agent/cli/cloud/commands/secrets/main.py
New Typer command group cloud secrets with list, add, remove, and pull commands for post-deploy secret management and export.
CLI wiring & constants
src/mcp_agent/cli/cloud/main.py, src/mcp_agent/cli/core/constants.py, src/mcp_agent/cli/cloud/commands/utils.py
Registered secrets sub-typer, added MCP_DEPLOYED_CONFIG_FILENAME constant, and added get_app_defaults_from_config utility to extract name/description from config.
Tests
tests/cli/cloud/test_materialize.py, tests/config/test_env_settings.py
Added tests for materialization behavior (artifact creation, fallback handling, handle reuse, invalid YAML) and Settings.env validation/iteration tests.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant DeployCLI as Deploy CLI
    participant Config as Settings Loader
    participant Materialize as Materialize Module
    participant SecretsAPI as Secrets API
    participant FS as File System

    User->>DeployCLI: mcp-agent deploy
    DeployCLI->>Config: load mcp_agent.config.yaml
    Config-->>DeployCLI: Settings (env specs)
    DeployCLI->>Materialize: materialize_deployment_artifacts(...)
    Materialize->>Config: iter_env_specs()
    Config-->>Materialize: [("KEY", fallback?), ...]
    Materialize->>Materialize: resolve from os.environ or fallback
    Materialize->>SecretsAPI: create_secret / set_secret_value
    SecretsAPI-->>Materialize: secret handle
    Materialize->>FS: write mcp_agent.deployed.secrets.yaml
    Materialize->>FS: write mcp_agent.deployed.config.yaml
    Materialize-->>DeployCLI: deployed paths
    DeployCLI->>User: deployment complete
Loading
sequenceDiagram
    participant User
    participant SecretsCLI as cloud secrets
    participant SecretsAPI as Secrets API
    participant FS as File System

    User->>SecretsCLI: cloud secrets list
    SecretsCLI->>SecretsAPI: list handles
    SecretsAPI-->>SecretsCLI: handles
    SecretsCLI-->>User: masked table

    User->>SecretsCLI: cloud secrets add KEY VALUE
    SecretsCLI->>SecretsAPI: create or update secret
    SecretsAPI-->>SecretsCLI: success

    User->>SecretsCLI: cloud secrets pull
    SecretsCLI->>SecretsAPI: fetch values
    SecretsAPI-->>SecretsCLI: values
    SecretsCLI->>FS: write mcp_agent.cloud.secrets.yaml
    FS-->>User: file written
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Pay extra attention to:
    • src/mcp_agent/cli/cloud/commands/deploy/materialize.py (env resolution, SecretsClient interactions, non-interactive behavior).
    • src/mcp_agent/cli/cloud/commands/deploy/main.py (integration points and sequencing before git-tagging).
    • src/mcp_agent/config.py (validator edge cases and iteration order).
    • Tests tests/cli/cloud/test_materialize.py for coverage of created/updated secret paths and file persistence.

Possibly related PRs

Suggested reviewers

  • rholinshead
  • petersonbill64

Poem

🐰 A rabbit skips through configs bright,
Capturing envs into the night,
Secrets tucked and written down,
Deployed configs wear the crown—
Hooray for bindings done just right! ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.48% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title clearly summarizes the three main components of this changeset: (1) enabling environment variables to be stored and managed as secrets, (2) introducing a new app-scoped cloud secrets command group, and (3) materializing a deployed config snapshot file. All three aspects mentioned in the title are explicitly present throughout the changes—from the new Settings.env field and validation logic, to the new secrets command module with list/add/remove/pull subcommands, to the materialize_deployment_artifacts function. The title is specific and descriptive enough that a teammate reviewing the commit history would immediately understand the primary purpose of the PR without ambiguity or vagueness.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/env_secrets

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.

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: 2

🧹 Nitpick comments (4)
tests/config/test_env_settings.py (1)

6-17: Test coverage is adequate for basic scenarios.

The tests correctly validate the core functionality of iter_env_specs and empty string rejection. However, consider adding test cases for additional edge cases to strengthen validation:

  • Dict with multiple keys (should raise ValueError per validation logic)
  • Non-string/non-dict entries in the env list
  • Dict with empty key (whitespace-only after strip)
  • Empty dict {}

Example additions:

def test_env_validation_rejects_dict_with_multiple_keys():
    with pytest.raises(ValueError, match="exactly one key-value pair"):
        Settings(env=[{"KEY1": "val1", "KEY2": "val2"}])

def test_env_validation_rejects_invalid_types():
    with pytest.raises(ValueError, match="must be specified as strings or single-key mappings"):
        Settings(env=[123])

def test_env_validation_rejects_whitespace_only_key():
    with pytest.raises(ValueError, match="must be non-empty strings"):
        Settings(env=["  "])
src/mcp_agent/cli/cloud/commands/secrets/main.py (2)

148-199: Consider validating environment variable key format.

The command correctly handles create/update logic and provides clear feedback. However, it doesn't validate that key is a valid environment variable name.

Consider adding validation after Line 150:

def add_secret(
    key: str = typer.Argument(..., help="Environment variable to store as a secret"),
    value: str = typer.Argument(..., help="Secret value to store"),
    # ... rest of parameters
) -> None:
    """Create or update an environment secret."""
    # Validate key format
    if not key or not key.strip():
        raise CLIError("Environment variable key must be non-empty.")
    
    import re
    if not re.match(r'^[A-Z_][A-Z0-9_]*$', key):
        print_info(
            f"Warning: '{key}' doesn't follow standard environment variable naming conventions (A-Z, 0-9, underscore)."
        )
    
    if not value:
        raise CLIError("Secret value must be non-empty.")

This prevents typos and ensures consistency with common environment variable naming conventions.


243-311: Consider error handling when fetching secret values.

The command has good UX with confirmation prompts and proper YAML formatting. However, if get_secret_value fails for any handle, the entire command will fail without partial results.

Consider wrapping the fetch loop with error handling:

resolved: Dict[str, str] = {}
failed_keys: list[str] = []
for key, handle in handles.items():
    try:
        value = run_async(client.get_secret_value(handle))
        resolved[key] = value
    except Exception as e:
        failed_keys.append(key)
        print_error(f"Failed to fetch secret for {key}: {e}")

if failed_keys:
    print_error(f"Failed to fetch {len(failed_keys)} secret(s). Proceeding with {len(resolved)} successful fetches.")
    if not resolved:
        raise typer.Exit(1)

This provides partial results and clearer diagnostics when individual secrets are inaccessible.

src/mcp_agent/app.py (1)

246-265: Consider adding debug logging for environment binding operations.

The implementation correctly handles fallback values and skips secret handles. However, the absence of logging makes it difficult to debug when environment variables aren't being set as expected.

Consider adding debug logging:

 def _apply_environment_bindings(self) -> None:
     """Populate os.environ with values declared in settings.env when the value is available."""
     try:
         specs = list(self._config.iter_env_specs())
     except Exception:
         return
 
     for key, value in specs:
         if not key:
             continue
         if os.environ.get(key):
+            # Could add: self.logger.debug(f"Skipping {key}: already set in environment")
             continue
         if value is None:
+            # Could add: self.logger.debug(f"Skipping {key}: no fallback value")
             continue
         str_value = str(value)
         if str_value.startswith("mcpac_sc_"):
             # Actual secret values are injected by the deployment environment; skip handles.
+            # Could add: self.logger.debug(f"Skipping {key}: secret handle placeholder")
             continue
         os.environ[key] = str_value
+        # Could add: self.logger.debug(f"Set environment variable {key}")

This helps diagnose configuration issues during initialization.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 27b2a4d and d58bee5.

📒 Files selected for processing (14)
  • docs/cloud/deployment-quickstart.mdx (3 hunks)
  • docs/cloud/mcp-agent-cloud/manage-secrets.mdx (3 hunks)
  • docs/reference/cli.mdx (3 hunks)
  • src/mcp_agent/app.py (1 hunks)
  • src/mcp_agent/cli/cloud/commands/deploy/main.py (6 hunks)
  • src/mcp_agent/cli/cloud/commands/deploy/materialize.py (1 hunks)
  • src/mcp_agent/cli/cloud/commands/secrets/__init__.py (1 hunks)
  • src/mcp_agent/cli/cloud/commands/secrets/main.py (1 hunks)
  • src/mcp_agent/cli/cloud/commands/utils.py (3 hunks)
  • src/mcp_agent/cli/cloud/main.py (2 hunks)
  • src/mcp_agent/cli/core/constants.py (1 hunks)
  • src/mcp_agent/config.py (3 hunks)
  • tests/cli/cloud/test_materialize.py (1 hunks)
  • tests/config/test_env_settings.py (1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-07-22T18:59:49.368Z
Learnt from: CR
Repo: lastmile-ai/mcp-agent PR: 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/core/constants.py
  • docs/cloud/deployment-quickstart.mdx
📚 Learning: 2025-08-28T15:07:10.015Z
Learnt from: saqadri
Repo: lastmile-ai/mcp-agent PR: 386
File: src/mcp_agent/mcp/mcp_server_registry.py:110-116
Timestamp: 2025-08-28T15:07:10.015Z
Learning: In MCP server registry methods, when client_session_factory parameters are updated to accept additional context parameters, ensure the type hints match what is actually passed (Context instance vs ServerSession) and that the default factory (MCPAgentClientSession) can handle the number of arguments being passed to avoid TypeError at runtime.

Applied to files:

  • src/mcp_agent/app.py
📚 Learning: 2025-07-22T18:59:49.368Z
Learnt from: CR
Repo: lastmile-ai/mcp-agent PR: 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/**/*.py : Use mcp-agent's Agent abstraction for ALL LLM interactions, including quality evaluation, to ensure consistent tool access, logging, and error handling.

Applied to files:

  • src/mcp_agent/app.py
🧬 Code graph analysis (7)
src/mcp_agent/cli/cloud/commands/utils.py (4)
src/mcp_agent/app.py (1)
  • config (191-192)
src/mcp_agent/config.py (1)
  • get_settings (1340-1463)
tests/cli/cloud/test_materialize.py (1)
  • config_file (27-30)
src/mcp_agent/core/context.py (2)
  • name (235-238)
  • description (241-244)
tests/cli/cloud/test_materialize.py (1)
src/mcp_agent/cli/cloud/commands/deploy/materialize.py (1)
  • materialize_deployment_artifacts (90-192)
src/mcp_agent/app.py (1)
src/mcp_agent/config.py (1)
  • iter_env_specs (1262-1270)
src/mcp_agent/cli/cloud/commands/deploy/main.py (3)
src/mcp_agent/cli/secrets/api_client.py (1)
  • SecretsClient (12-206)
src/mcp_agent/cli/cloud/commands/utils.py (1)
  • get_app_defaults_from_config (181-203)
src/mcp_agent/cli/cloud/commands/deploy/materialize.py (1)
  • materialize_deployment_artifacts (90-192)
src/mcp_agent/cli/cloud/commands/deploy/materialize.py (4)
src/mcp_agent/cli/exceptions.py (1)
  • CLIError (4-10)
src/mcp_agent/cli/secrets/api_client.py (1)
  • SecretsClient (12-206)
src/mcp_agent/cli/secrets/yaml_tags.py (2)
  • dump_yaml_with_secrets (110-126)
  • load_yaml_with_secrets (97-107)
src/mcp_agent/config.py (2)
  • Settings (1094-1270)
  • iter_env_specs (1262-1270)
tests/config/test_env_settings.py (1)
src/mcp_agent/config.py (2)
  • Settings (1094-1270)
  • iter_env_specs (1262-1270)
src/mcp_agent/cli/cloud/commands/secrets/main.py (7)
src/mcp_agent/cli/auth/main.py (1)
  • load_api_key_credentials (108-115)
src/mcp_agent/cli/cloud/commands/utils.py (2)
  • get_app_defaults_from_config (181-203)
  • resolve_server (97-101)
src/mcp_agent/cli/exceptions.py (1)
  • CLIError (4-10)
src/mcp_agent/cli/mcp_app/api_client.py (2)
  • MCPAppClient (133-767)
  • get_app_by_name (351-373)
src/mcp_agent/cli/core/constants.py (1)
  • SecretType (36-40)
src/mcp_agent/cli/secrets/api_client.py (1)
  • SecretsClient (12-206)
src/mcp_agent/cli/utils/ux.py (3)
  • print_error (110-122)
  • print_info (44-62)
  • print_success (80-92)
🪛 LanguageTool
docs/cloud/mcp-agent-cloud/manage-secrets.mdx

[uncategorized] ~72-~72: Do not mix variants of the same word (‘materialise’ and ‘materialize’) within a single text.
Context: ....environ[KEY]`; the cloud control plane materialises the secret before your app starts. ## ...

(EN_WORD_COHERENCY)

🔇 Additional comments (15)
src/mcp_agent/cli/cloud/commands/utils.py (1)

181-203: LGTM! Clean utility function with proper defensive checks.

The implementation correctly:

  • Handles missing or non-existent config files
  • Uses set_global=False to avoid modifying global state
  • Validates extracted values are non-empty strings
  • Provides graceful fallback with broad exception handling
src/mcp_agent/cli/cloud/commands/secrets/main.py (6)

1-30: LGTM! Standard Typer app setup with appropriate imports.

The imports are well-organized and the Typer app configuration is appropriate for a command group.


33-47: LGTM! Proper credential resolution and client construction.

The credential resolution chain (option → env → stored) is appropriate, and error handling provides clear user guidance.


49-81: LGTM! Robust app resolution with good fallback logic.

The function correctly:

  • Resolves from explicit identifier with both MCPApp and MCPAppConfiguration handling
  • Falls back to config defaults when no identifier provided
  • Provides clear error messages for resolution failures

83-99: LGTM! Defensive secret handle loading with proper validation.

The implementation correctly:

  • Uses namespaced prefix pattern for app-scoped secrets
  • Handles API response variations (secretId vs secret_id)
  • Validates secret names match expected prefix before processing

101-146: LGTM! Clean list command with good UX.

The implementation provides:

  • Clear informational message when no secrets exist
  • Well-formatted table output
  • Proper handle masking for security
  • Consistent ordering via sorting

201-241: LGTM! Clean remove command with proper error handling.

The implementation correctly:

  • Validates secret existence before attempting deletion
  • Provides clear error messages
  • Uses appropriate exit code for user-facing errors
src/mcp_agent/cli/cloud/commands/secrets/__init__.py (1)

1-5: LGTM! Standard module initialization pattern.

Clean public API exposure for the secrets command group.

src/mcp_agent/cli/core/constants.py (1)

15-15: LGTM! Consistent constant naming and placement.

The new constant follows the established naming convention and is logically grouped with related deployment artifact filenames.

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

31-31: LGTM! Clean import following established patterns.


161-161: LGTM! Proper registration of secrets command group.

The secrets subcommand is correctly registered with clear help text and follows the established pattern for other command groups.

docs/cloud/deployment-quickstart.mdx (3)

61-61: LGTM! Clear documentation of both secret provisioning methods.

The update correctly describes the two approaches for managing secrets and provides a helpful link to detailed guidance.


83-84: LGTM! Important guidance on deployment artifacts.

The note correctly explains the purpose of the deployed artifact files and provides good advice about committing them for reproducible deployments.


108-109: LGTM! Helpful documentation of post-deployment secret management.

The section correctly documents the secrets management commands and importantly notes their CI-safety, which is valuable for automation scenarios.

src/mcp_agent/app.py (1)

271-272: LGTM! Correct placement ensures environment variables are available early.

Calling _apply_environment_bindings before context initialization ensures that any downstream code can access the populated environment variables.

Comment on lines +169 to +178
if handle:
run_async(secrets_client.set_secret_value(handle, value))
else:
handle = run_async(
secrets_client.create_secret(
name=secret_name,
secret_type=SecretType.DEVELOPER,
value=value,
)
)
Copy link

@coderabbitai coderabbitai bot Oct 31, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Handle secret update failures explicitly.

SecretsClient.set_secret_value returns a boolean success flag, but the current code ignores it. If the API responds with {"success": false} (e.g., handle revoked or backend failure), we silently proceed and persist the stale handle, leaving deployments with an outdated value and no warning. Capture the return value and raise a CLIError when the update fails so deploys cannot succeed with a dropped secret.

-        if handle:
-            run_async(secrets_client.set_secret_value(handle, value))
+        if handle:
+            success = run_async(secrets_client.set_secret_value(handle, value))
+            if not success:
+                raise CLIError(
+                    f"Failed to update secret handle '{handle}' for '{spec.key}'. "
+                    "Retry the deployment or re-materialize the secret."
+                )
📝 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
if handle:
run_async(secrets_client.set_secret_value(handle, value))
else:
handle = run_async(
secrets_client.create_secret(
name=secret_name,
secret_type=SecretType.DEVELOPER,
value=value,
)
)
if handle:
success = run_async(secrets_client.set_secret_value(handle, value))
if not success:
raise CLIError(
f"Failed to update secret handle '{handle}' for '{spec.key}'. "
"Retry the deployment or re-materialize the secret."
)
else:
handle = run_async(
secrets_client.create_secret(
name=secret_name,
secret_type=SecretType.DEVELOPER,
value=value,
)
)
🤖 Prompt for AI Agents
In src/mcp_agent/cli/cloud/commands/deploy/materialize.py around lines 169 to
178, the code calls secrets_client.set_secret_value but ignores its boolean
result; update failures should abort and not persist a stale handle. Change the
branch so you capture the return value from
run_async(secrets_client.set_secret_value(handle, value)); if it returns False,
raise a CLIError with a clear message (including secret_name and handle) and do
not proceed to persist or use the existing handle; only on True continue, and
keep the existing logic for creating a new secret when handle is missing.

Copy link
Member

Choose a reason for hiding this comment

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

Probably legit

Copy link

Choose a reason for hiding this comment

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

@rholinshead Thanks for confirming! The suggestion should help catch secret update failures early during deployment rather than silently continuing with stale handles. Let me know if you need any clarification on the implementation.

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 (1)
src/mcp_agent/cli/cloud/commands/deploy/materialize.py (1)

181-190: Handle secret update failures explicitly.

The return value from set_secret_value is ignored. If the API call fails (e.g., handle revoked, backend error), the deployment proceeds silently with a stale secret, which could cause runtime failures.

Apply this diff to check the return value and fail the deployment when secret updates fail:

         if handle:
-            run_async(secrets_client.set_secret_value(handle, value))
+            success = run_async(secrets_client.set_secret_value(handle, value))
+            if not success:
+                raise CLIError(
+                    f"Failed to update secret '{secret_name}' (handle: {handle}). "
+                    "The handle may be revoked or invalid. Retry the deployment."
+                )
         else:
             handle = run_async(
🧹 Nitpick comments (1)
tests/cli/cloud/test_materialize.py (1)

12-24: Consider adding failure scenario tests for secret operations.

The FakeSecretsClient always returns True from set_secret_value, which means failure scenarios (e.g., revoked handles, API errors) aren't covered. Since the implementation doesn't currently check the return value (per the past review comment on the implementation file), adding tests for this failure path would help ensure robust error handling once that issue is addressed.

Consider adding a test like:

def test_materialize_handles_secret_update_failure(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
    cfg = tmp_path / "mcp_agent.config.yaml"
    cfg.write_text("env:\n  - OPENAI_API_KEY\n", encoding="utf-8")
    deployed_secrets = tmp_path / "mcp_agent.deployed.secrets.yaml"
    deployed_secrets.write_text(
        yaml.safe_dump({"env": [{"OPENAI_API_KEY": "mcpac_sc_old"}]}),
        encoding="utf-8",
    )
    
    class FailingSecretsClient(FakeSecretsClient):
        async def set_secret_value(self, handle, value):
            return False  # Simulate failure
    
    monkeypatch.setenv("OPENAI_API_KEY", "new-value")
    client = FailingSecretsClient()
    
    with pytest.raises(CLIError, match="Failed to update secret"):
        materialize_deployment_artifacts(
            config_dir=tmp_path,
            app_id="app_123",
            config_file=cfg,
            deployed_secrets_path=deployed_secrets,
            secrets_client=client,
            non_interactive=True,
        )
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d58bee5 and 69a367a.

📒 Files selected for processing (2)
  • src/mcp_agent/cli/cloud/commands/deploy/materialize.py (1 hunks)
  • tests/cli/cloud/test_materialize.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/mcp_agent/cli/cloud/commands/deploy/materialize.py (6)
src/mcp_agent/cli/exceptions.py (1)
  • CLIError (4-10)
src/mcp_agent/cli/secrets/api_client.py (1)
  • SecretsClient (12-206)
src/mcp_agent/cli/secrets/yaml_tags.py (2)
  • dump_yaml_with_secrets (110-126)
  • load_yaml_with_secrets (97-107)
src/mcp_agent/app.py (1)
  • config (191-192)
src/mcp_agent/config.py (3)
  • Settings (1094-1270)
  • get_settings (1340-1463)
  • iter_env_specs (1262-1270)
tests/cli/cloud/test_materialize.py (4)
  • config_file (28-31)
  • set_secret_value (22-24)
  • create_secret (17-20)
  • create_secret (96-97)
tests/cli/cloud/test_materialize.py (1)
src/mcp_agent/cli/cloud/commands/deploy/materialize.py (1)
  • materialize_deployment_artifacts (92-204)
🔇 Additional comments (14)
tests/cli/cloud/test_materialize.py (5)

34-57: LGTM!

The test effectively validates the basic materialization flow: env vars are resolved from the environment, deployed artifacts are created, and secret handles have the expected format.


59-81: LGTM!

The test correctly validates fallback behavior when environment variables aren't set.


83-112: LGTM!

The test validates idempotency correctly—existing secret handles are reused and updated rather than creating duplicates.


114-133: LGTM!

The test ensures graceful degradation when config files are malformed—returning the original config path and creating an empty secrets file prevents deployment failures.


135-169: LGTM!

The test validates the correct precedence hierarchy: MCPApp configuration takes priority over file-based config, which is the expected behavior.

src/mcp_agent/cli/cloud/commands/deploy/materialize.py (9)

1-35: LGTM!

The imports and EnvSpec dataclass are well-structured. Using slots=True is a good optimization for this simple data holder.


37-71: LGTM!

The helper functions are well-designed with appropriate defensive checks. The _load_deployed_secrets function correctly handles missing files, and _extract_existing_env_handles validates the structure before extracting values.


74-89: LGTM!

The config persistence logic correctly materializes only non-default, non-None, and non-unset values, keeping the deployed configuration minimal and focused.


92-136: LGTM!

The function correctly handles the config loading hierarchy and gracefully degrades when configuration is invalid—ensuring empty secrets files are created to prevent downstream issues.


138-145: LGTM!

The early return when no environment specifications exist is a good optimization, while still ensuring consistency by creating an empty secrets file if needed.


147-176: LGTM!

The environment variable resolution logic correctly handles the precedence hierarchy (environment → fallback → prompt) and validates that resolved values are non-empty.


192-204: LGTM!

The finalization logic correctly assembles the normalized entries and persists them. The user notification (lines 194-199) when values are manually captured provides good feedback.


207-227: Verify module cleanup on all exit paths.

The function manipulates sys.modules and sys.path but only guarantees cleanup of sys.path via the finally block. If an exception occurs after del sys.modules[module_name] (line 218) but before successful import, the module remains deleted from sys.modules, which could affect subsequent operations.

Consider whether sys.modules should be restored in the finally block:

finally:
    if added_path and module_path in sys.path:
        try:
            sys.path.remove(module_path)
        except ValueError:
            pass
    # Consider: restore sys.modules[module_name] if it was present initially?

Alternatively, verify that the current behavior (leaving the module deleted) is acceptable for the CLI context, where module state typically doesn't persist across operations.


228-264: LGTM!

The MCPApp discovery logic correctly validates that exactly one instance exists and provides clear user feedback for error cases. The exception handling is comprehensive and user-friendly.

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.

Overall looks good to me but I have a strong suspicion the config materialization is going to surface exceptions for urls in the config, so we should resolve that before landing

Use the `cloud secrets` commands to inspect or rotate values captured via the `env` list:

```bash
uvx mcp-agent cloud secrets list my-app
Copy link
Member

Choose a reason for hiding this comment

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

Is my-app the name?

Copy link
Member

Choose a reason for hiding this comment

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

Ah, ok, looks like it's any of the identifiers (name,id,server)

2. Stores it as a deployment secret named `apps/<app_id>/env/<KEY>`.
3. Inserts the resulting handle into the `env:` list inside `mcp_agent.deployed.secrets.yaml`.

The runtime still references the value via `os.environ[KEY]`; the cloud control plane materialises the secret before your app starts.
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 do the materialization part in the deployment backend still, right?

Copy link
Member

Choose a reason for hiding this comment

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

Oh, actually I think below with the env handling from the config in mcp-agent it should already be handled because the handle will already be resolved as normal in the deployed secrets file and then the env specs are put back into env by mcp-agent itself

- `list` masks secret handles so you can confirm which keys are present.
- `add` updates the stored value (under the hood it calls the Secrets API and rewrites the handle if needed).
- `remove` deletes the value entirely.
- `pull` resolves values to a local YAML file (`mcp_agent.cloud.secrets.yaml` by default) without touching `mcp_agent.secrets.yaml`, mirroring Vercel’s `env pull`.
Copy link
Member

Choose a reason for hiding this comment

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

What do you do with the cloud.secrets yaml afterwards? Wondering if it's a bit too much complexity having yet another yaml file vs just printing to the console or something


Use `--dry-run` for validation without uploading, and let the CLI guide you through classifying developer versus user secrets.

When deployment completes you will see two new artefacts next to your source:
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
When deployment completes you will see two new artefacts next to your source:
When deployment completes you will see two new artifacts next to your source:

- `--dry-run` validates using mock clients without persisting anything.

- **Secrets (`mcp-agent cloud secrets …`)** – manage environment secrets captured during deploy.
- `secrets list [APP]` displays stored environment keys with masked handles.
Copy link
Member

Choose a reason for hiding this comment

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

Just noting the possibility for confusion with these commands being named 'secrets' but actually specifically being env secrets and not general secrets that may be used/stored in the secrets config; would something like env, env_vars or env_secrets be a clearer command name?

exclude_defaults=not include_overrides,
)
with open(config_path, "w", encoding="utf-8") as handle:
yaml.safe_dump(materialized, handle, default_flow_style=False, sort_keys=False)
Copy link
Member

Choose a reason for hiding this comment

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

Can you double-check this works with ouath examples? For context, when trying to deploy oauth/auth config settings to cloud on Friday I was seeing an error resulting from the AnyHttpUrl types in the config, which can't serialize properly. I didn't have enough time to fully fix in the deployment daemon (will continue on that tomorrow) - but it would either require custom dump handling or use mode="json". The latter adds an extra "/" at the end, though. But in any case, my understanding is safe_dump will actually raise an exception instead, so apps with oauth wouldn't be able to be deployed without error during materialization

Copy link
Member

Choose a reason for hiding this comment

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

Side-note, fwiw claude thinks mode="json" would be safe except for specific provider handling:

Based on this detailed analysis, here's what I found:

  🎯 The Good News: Most Critical Paths Are Safe

  ✅ Cache Keys - All Canonicalized

  - Resource metadata cache: URLs go through normalize_resource() first
  - Authorization metadata cache: URLs go through _canonicalize_url() first
  - Verdict: Trailing slashes won't cause cache issues

  ✅ Token Storage - Indirectly Canonicalized

  - Resource URLs: Processed by normalize_resource() before storage
  - Authorization server URLs: Processed by _canonicalize_url() before storage
  - Verdict: Trailing slashes won't cause token storage mismatches

  ✅ URL Comparisons - Both Sides Canonicalized

  - Authorization server validation canonicalizes both the provided URL and the resolved issuer
  - Verdict: Trailing slashes won't cause comparison failures

  ⚠️ The Potential Issues

  1. OAuth Provider API Calls

  # These are used RAW from metadata/config:
  authorize_url = httpx.URL(str(auth_metadata.authorization_endpoint) + "?" + params)
  token_endpoint = str(auth_metadata.token_endpoint)
  redirect_uri = redirect_uri  # From config

  Impact: If OAuth providers do strict string matching on redirect URIs, a mismatch between registered https://example.com/callback and Pydantic-normalized
  https://example.com/callback/ could cause failures.

  2. Callback URL Registration

  When you register your OAuth app with a provider, you specify exact callback URLs. If:
  - You register: https://example.com/callback
  - But Pydantic normalizes to: https://example.com/callback/
  - And that gets sent to the OAuth provider

  Then the OAuth provider might reject it due to URL mismatch.

  🎯 Conclusion

  The trailing slash issue is likely NOT a problem for most functionality because:

  1. Internal caching and storage is safe - everything gets canonicalized
  2. URL comparisons are safe - both sides get canonicalized
  3. Most URL construction is safe - rstrip('/') handles both cases

  The only real risk is OAuth provider callback URL validation, which depends on:
  - Whether OAuth providers do exact string matching on redirect URIs
  - Whether the registered callback URLs match what gets sent after Pydantic normalization

  For most OAuth providers, this probably isn't an issue since they typically normalize URLs themselves. But it's worth testing with actual OAuth flows to be sure.

Copy link
Member

Choose a reason for hiding this comment

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

Ok, #605 should be all we need to allow us to just use mode="json" here when yaml dumping

Comment on lines +139 to +140
# Nothing further to do; ensure secrets file exists if previously created
if not deployed_secrets_path.exists():
Copy link
Member

Choose a reason for hiding this comment

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

Can you clarify this case? It seems to just be creating an empty deployed secrets file if it doesn't exist yet. But for apps that don't have secrets, we wouldn't want that, right?

Comment on lines +169 to +178
if handle:
run_async(secrets_client.set_secret_value(handle, value))
else:
handle = run_async(
secrets_client.create_secret(
name=secret_name,
secret_type=SecretType.DEVELOPER,
value=value,
)
)
Copy link
Member

Choose a reason for hiding this comment

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

Probably legit

module_file = Path(getattr(module, "__file__", "")).resolve()
if not module_file or project_root not in module_file.parents:
typer.secho(
f"Module 'main' resolved outside project directory ({module_file}); skipping MCPApp load.",
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 sys/module manipulation an additional security over checking main.py exists?


def _load_existing_handles(client: SecretsClient, app_id: str) -> Dict[str, str]:
prefix = _env_secret_prefix(app_id)
secrets = run_async(client.list_secrets(name_filter=prefix))
Copy link
Member

Choose a reason for hiding this comment

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

(later) We can also add the association between the app and secrets in the DB to be able to query more efficiently

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