From 4a737b9710a33480588c74ccb532ec755d0e04d1 Mon Sep 17 00:00:00 2001
From: Eric Gustin <34000337+EricGustin@users.noreply.github.com>
Date: Wed, 25 Feb 2026 23:20:28 -0800
Subject: [PATCH] Improve `.env` discovery (#737)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Resolves TOO-201
Documentation PR for this is here:
https://github.com/ArcadeAI/docs/pull/626
---
> [!NOTE]
> **Medium Risk**
> Changes how environment variables/secrets are discovered and loaded,
which can subtly alter runtime behavior depending on directory structure
and existing env vars; bounded traversal and added tests reduce but
don’t eliminate this risk.
>
> **Overview**
> **Improves `.env` discovery across the MCP server and CLI.** Adds
`find_env_file()` (bounded by the nearest `pyproject.toml` by default)
and switches settings loading, `arcade deploy`, `arcade configure` stdio
env injection, and provider API-key resolution to use it.
>
> Updates dev reload to also watch the discovered `.env` even when it
lives outside the current working directory, adjusts `deploy --secrets
all` to only run when a `.env` was found, and moves the minimal
scaffold’s `.env.example` to the project root with updated
tests/integration checks. Version bumps align examples and top-level
deps with `arcade-mcp-server` `1.17.4` and `arcade-mcp` `1.11.2`.
>
> Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
40cff1738c14674ce01f09fd325ece9c874cd072. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).
---------
Co-authored-by: Claude Sonnet 4.6
---
.../{src/authorization => }/.env.example | 0
.../mcp_servers/authorization/pyproject.toml | 4 +-
.../simple/{src/simple => }/.env.example | 0
examples/mcp_servers/simple/pyproject.toml | 4 +-
.../{src/tool_chaining => }/.env.example | 0
.../mcp_servers/tool_chaining/pyproject.toml | 4 +-
libs/arcade-cli/arcade_cli/configure.py | 15 ++-
libs/arcade-cli/arcade_cli/deploy.py | 13 +-
.../minimal/{{ toolkit_name }}/.env.example | 13 ++
.../src/{{ toolkit_name }}/.env.example | 1 -
libs/arcade-cli/arcade_cli/utils.py | 7 +-
.../arcade_mcp_server/mcp_app.py | 16 ++-
.../arcade_mcp_server/settings.py | 91 +++++++++++++-
libs/arcade-mcp-server/pyproject.toml | 2 +-
.../arcade_mcp_server/test_env_discovery.py | 111 ++++++++++++++++++
libs/tests/cli/test_configure.py | 25 ++++
libs/tests/cli/test_new_cli.py | 2 +-
libs/tests/conftest.py | 22 ++--
pyproject.toml | 4 +-
tests/integration/no_auth_cli_smoke.py | 2 +-
20 files changed, 292 insertions(+), 44 deletions(-)
rename examples/mcp_servers/authorization/{src/authorization => }/.env.example (100%)
rename examples/mcp_servers/simple/{src/simple => }/.env.example (100%)
rename examples/mcp_servers/tool_chaining/{src/tool_chaining => }/.env.example (100%)
create mode 100644 libs/arcade-cli/arcade_cli/templates/minimal/{{ toolkit_name }}/.env.example
delete mode 100644 libs/arcade-cli/arcade_cli/templates/minimal/{{ toolkit_name }}/src/{{ toolkit_name }}/.env.example
create mode 100644 libs/tests/arcade_mcp_server/test_env_discovery.py
diff --git a/examples/mcp_servers/authorization/src/authorization/.env.example b/examples/mcp_servers/authorization/.env.example
similarity index 100%
rename from examples/mcp_servers/authorization/src/authorization/.env.example
rename to examples/mcp_servers/authorization/.env.example
diff --git a/examples/mcp_servers/authorization/pyproject.toml b/examples/mcp_servers/authorization/pyproject.toml
index cbfb6dcf..6fe3d5cd 100644
--- a/examples/mcp_servers/authorization/pyproject.toml
+++ b/examples/mcp_servers/authorization/pyproject.toml
@@ -4,13 +4,13 @@ version = "0.1.0"
description = "MCP Server created with Arcade.dev"
requires-python = ">=3.10"
dependencies = [
- "arcade-mcp-server>=1.12.0,<2.0.0",
+ "arcade-mcp-server>=1.17.4,<2.0.0",
"httpx>=0.28.0,<1.0.0",
]
[project.optional-dependencies]
dev = [
- "arcade-mcp[all]>=1.5.2,<2.0.0",
+ "arcade-mcp[all]>=1.11.2,<2.0.0",
"pytest>=7.0.0",
"pytest-asyncio>=0.21.0",
"mypy>=1.0.0",
diff --git a/examples/mcp_servers/simple/src/simple/.env.example b/examples/mcp_servers/simple/.env.example
similarity index 100%
rename from examples/mcp_servers/simple/src/simple/.env.example
rename to examples/mcp_servers/simple/.env.example
diff --git a/examples/mcp_servers/simple/pyproject.toml b/examples/mcp_servers/simple/pyproject.toml
index 85227a7f..7646030b 100644
--- a/examples/mcp_servers/simple/pyproject.toml
+++ b/examples/mcp_servers/simple/pyproject.toml
@@ -4,13 +4,13 @@ version = "0.1.0"
description = "MCP Server created with Arcade.dev"
requires-python = ">=3.10"
dependencies = [
- "arcade-mcp-server>=1.5.0,<2.0.0",
+ "arcade-mcp-server>=1.17.4,<2.0.0",
"httpx>=0.28.0,<1.0.0",
]
[project.optional-dependencies]
dev = [
- "arcade-mcp[all]>=1.4.0,<2.0.0",
+ "arcade-mcp[all]>=1.11.2,<2.0.0",
"pytest>=7.0.0",
"pytest-asyncio>=0.21.0",
"mypy>=1.0.0",
diff --git a/examples/mcp_servers/tool_chaining/src/tool_chaining/.env.example b/examples/mcp_servers/tool_chaining/.env.example
similarity index 100%
rename from examples/mcp_servers/tool_chaining/src/tool_chaining/.env.example
rename to examples/mcp_servers/tool_chaining/.env.example
diff --git a/examples/mcp_servers/tool_chaining/pyproject.toml b/examples/mcp_servers/tool_chaining/pyproject.toml
index 52905f18..ba19aba5 100644
--- a/examples/mcp_servers/tool_chaining/pyproject.toml
+++ b/examples/mcp_servers/tool_chaining/pyproject.toml
@@ -4,13 +4,13 @@ version = "0.1.0"
description = "MCP Server created with Arcade.dev"
requires-python = ">=3.10"
dependencies = [
- "arcade-mcp-server>=1.5.0,<2.0.0",
+ "arcade-mcp-server>=1.17.4,<2.0.0",
"httpx>=0.28.0,<1.0.0",
]
[project.optional-dependencies]
dev = [
- "arcade-mcp[all]>=1.4.0,<2.0.0",
+ "arcade-mcp[all]>=1.11.2,<2.0.0",
"pytest>=7.0.0",
"pytest-asyncio>=0.21.0",
"mypy>=1.0.0",
diff --git a/libs/arcade-cli/arcade_cli/configure.py b/libs/arcade-cli/arcade_cli/configure.py
index 05b0a592..68ca07a4 100644
--- a/libs/arcade-cli/arcade_cli/configure.py
+++ b/libs/arcade-cli/arcade_cli/configure.py
@@ -10,6 +10,7 @@ import subprocess
from pathlib import Path
import typer
+from arcade_mcp_server.settings import find_env_file
from dotenv import dotenv_values
from arcade_cli.console import console
@@ -204,10 +205,16 @@ def is_uv_installed() -> bool:
def get_tool_secrets() -> dict:
- """Only useful for stdio servers, because HTTP servers load in envvars at runtime"""
- # TODO: Allow for a custom .env file to be used
- env_path = Path.cwd() / ".env"
- if env_path.exists():
+ """Get tool secrets from .env file for stdio servers.
+
+ Discovers .env file by traversing upward from the current directory
+ through parent directories until a .env file is found.
+
+ Returns:
+ Dictionary of environment variables from the .env file, or empty dict if not found.
+ """
+ env_path = find_env_file()
+ if env_path is not None:
return dotenv_values(env_path)
return {}
diff --git a/libs/arcade-cli/arcade_cli/deploy.py b/libs/arcade-cli/arcade_cli/deploy.py
index 715bf4d3..28d56318 100644
--- a/libs/arcade-cli/arcade_cli/deploy.py
+++ b/libs/arcade-cli/arcade_cli/deploy.py
@@ -16,6 +16,7 @@ from arcade_core.subprocess_utils import (
get_windows_no_window_creationflags,
graceful_terminate_process,
)
+from arcade_mcp_server.settings import find_env_file
from dotenv import load_dotenv
from pydantic import BaseModel, Field
from rich.columns import Columns
@@ -817,14 +818,14 @@ def deploy_server_logic(
)
console.print(f"✓ Entrypoint file found at {entrypoint_path}", style="green")
- # Step 3: Load .env file from current directory if it exists
- console.print("\nLoading .env file from current directory if it exists...", style="dim")
- env_path = current_dir / ".env"
- if env_path.exists():
+ # Step 3: Load .env file if it exists (searches upward through parent directories)
+ console.print("\nSearching for .env file...", style="dim")
+ env_path = find_env_file()
+ if env_path is not None:
load_dotenv(env_path, override=False)
console.print(f"✓ Loaded environment from {env_path}", style="green")
else:
- console.print(f"[!] No .env file found at {env_path}", style="yellow")
+ console.print("[!] No .env file found in current or parent directories", style="yellow")
# Step 4: Verify server and extract metadata (or skip if --skip-validate)
required_secrets_from_validation: set[str] = set()
@@ -860,7 +861,7 @@ def deploy_server_logic(
if secrets == "skip":
console.print("\n[!] Skipping secret upload (--secrets skip)", style="yellow")
- elif secrets == "all":
+ elif secrets == "all" and env_path is not None:
console.print("\nUploading ALL secrets from .env file...", style="dim")
secrets_to_upsert = set(load_env_file(str(env_path)).keys())
if secrets_to_upsert:
diff --git a/libs/arcade-cli/arcade_cli/templates/minimal/{{ toolkit_name }}/.env.example b/libs/arcade-cli/arcade_cli/templates/minimal/{{ toolkit_name }}/.env.example
new file mode 100644
index 00000000..c609e360
--- /dev/null
+++ b/libs/arcade-cli/arcade_cli/templates/minimal/{{ toolkit_name }}/.env.example
@@ -0,0 +1,13 @@
+# Environment variables for {{ toolkit_name }} MCP server
+#
+# Copy this file to .env and fill in your values:
+# cp .env.example .env
+#
+# The .env file will be automatically discovered when running your server,
+# even from subdirectories like src/{{ toolkit_name }}/.
+#
+# IMPORTANT: Never commit your .env file to version control!
+
+# Example secret used by the whisper_secret tool
+# Replace with your actual secret value
+MY_SECRET_KEY="Your tools can have secrets injected at runtime!"
diff --git a/libs/arcade-cli/arcade_cli/templates/minimal/{{ toolkit_name }}/src/{{ toolkit_name }}/.env.example b/libs/arcade-cli/arcade_cli/templates/minimal/{{ toolkit_name }}/src/{{ toolkit_name }}/.env.example
deleted file mode 100644
index fe5a7446..00000000
--- a/libs/arcade-cli/arcade_cli/templates/minimal/{{ toolkit_name }}/src/{{ toolkit_name }}/.env.example
+++ /dev/null
@@ -1 +0,0 @@
-MY_SECRET_KEY="Your tools can have secrets injected at runtime!"
diff --git a/libs/arcade-cli/arcade_cli/utils.py b/libs/arcade-cli/arcade_cli/utils.py
index f3ea6fd5..54901538 100644
--- a/libs/arcade-cli/arcade_cli/utils.py
+++ b/libs/arcade-cli/arcade_cli/utils.py
@@ -26,6 +26,7 @@ from arcade_core.discovery import (
from arcade_core.errors import ToolkitLoadError
from arcade_core.network.org_transport import build_org_scoped_http_client
from arcade_core.schema import ToolDefinition
+from arcade_mcp_server.settings import find_env_file
from arcadepy import (
NOT_GIVEN,
APIConnectionError,
@@ -1123,9 +1124,9 @@ def resolve_provider_api_key(provider: Provider, provider_api_key: str | None =
if api_key:
return api_key
- # Then check .env file in current working directory
- env_file_path = Path.cwd() / ".env"
- if env_file_path.exists():
+ # Then check .env file by traversing upward through parent directories
+ env_file_path = find_env_file()
+ if env_file_path is not None:
load_dotenv(env_file_path, override=False)
api_key = os.getenv(env_var_name)
if api_key:
diff --git a/libs/arcade-mcp-server/arcade_mcp_server/mcp_app.py b/libs/arcade-mcp-server/arcade_mcp_server/mcp_app.py
index 5864ac1e..8da039c4 100644
--- a/libs/arcade-mcp-server/arcade_mcp_server/mcp_app.py
+++ b/libs/arcade-mcp-server/arcade_mcp_server/mcp_app.py
@@ -31,7 +31,7 @@ from arcade_mcp_server.exceptions import ServerError
from arcade_mcp_server.logging_utils import intercept_standard_logging
from arcade_mcp_server.resource_server.base import ResourceServerValidator
from arcade_mcp_server.server import MCPServer
-from arcade_mcp_server.settings import MCPSettings, ServerSettings
+from arcade_mcp_server.settings import MCPSettings, ServerSettings, find_env_file
from arcade_mcp_server.types import Prompt, PromptMessage, Resource
from arcade_mcp_server.usage import ServerTracker
from arcade_mcp_server.worker import create_arcade_mcp, serve_with_force_quit
@@ -367,7 +367,7 @@ class MCPApp:
This method runs as the parent process that watches for file changes
and spawns/restarts child processes to run the actual server.
"""
- env_file_path = Path.cwd() / ".env"
+ env_file_path = find_env_file()
def start_server_process() -> subprocess.Popen:
"""Start a child process running the server."""
@@ -414,9 +414,17 @@ class MCPApp:
try:
def watch_filter(change: Any, path: str) -> bool:
- return path.endswith(".py") or (Path(path) == env_file_path)
+ # Watch Python files and the .env file (if one was found)
+ return path.endswith(".py") or (
+ env_file_path is not None and Path(path) == env_file_path
+ )
- for changes in watch(".", watch_filter=watch_filter):
+ # Watch current directory, plus the .env file if it's outside cwd
+ paths_to_watch: list[str] = ["."]
+ if env_file_path is not None:
+ paths_to_watch.append(str(env_file_path))
+
+ for changes in watch(*paths_to_watch, watch_filter=watch_filter):
logger.info(f"Detected changes in {len(changes)} file(s), restarting server...")
shutdown_server_process(process, reason="reload")
process = start_server_process()
diff --git a/libs/arcade-mcp-server/arcade_mcp_server/settings.py b/libs/arcade-mcp-server/arcade_mcp_server/settings.py
index e8e4b8c1..26ca72d7 100644
--- a/libs/arcade-mcp-server/arcade_mcp_server/settings.py
+++ b/libs/arcade-mcp-server/arcade_mcp_server/settings.py
@@ -12,6 +12,86 @@ from pydantic import Field, field_validator
from pydantic_settings import BaseSettings
+def _find_project_root(start_dir: Path) -> Path | None:
+ """Find the nearest ancestor directory containing pyproject.toml.
+
+ This is used as a default boundary for upward directory traversal
+ to prevent accidentally loading files from unrelated parent directories.
+
+ Args:
+ start_dir: Directory to start searching from (must be resolved).
+
+ Returns:
+ Path to the project root directory, or None if no pyproject.toml is found.
+ """
+ current = start_dir
+ while True:
+ if (current / "pyproject.toml").is_file():
+ return current
+ parent = current.parent
+ if parent == current:
+ return None
+ current = parent
+
+
+def find_env_file(
+ start_dir: Path | None = None,
+ stop_at: Path | None = None,
+ filename: str = ".env",
+) -> Path | None:
+ """Find a .env file by traversing upward through parent directories.
+
+ Starts at the specified directory (or current working directory) and
+ traverses upward through parent directories until a .env file is found
+ or a boundary is reached.
+
+ By default, traversal stops at the nearest ancestor directory containing
+ ``pyproject.toml`` (the project root). This prevents accidentally loading
+ an unrelated ``.env`` file from ``~/`` or other parent directories.
+ Pass an explicit ``stop_at`` to override this behavior.
+
+ Args:
+ start_dir: Directory to start searching from. Defaults to current working directory.
+ stop_at: Directory to stop traversal at (inclusive). If specified, the search
+ will not continue past this directory. The stop_at directory itself
+ is still checked for the .env file. When not specified, the nearest
+ ancestor containing ``pyproject.toml`` is used as the boundary.
+ filename: Name of the env file to find. Defaults to ".env".
+
+ Returns:
+ Path to the .env file if found, None otherwise.
+
+ Example:
+ # Find .env starting from current directory (bounded by pyproject.toml)
+ env_path = find_env_file()
+
+ # Find .env starting from a specific directory
+ env_path = find_env_file(start_dir=Path("/path/to/project/src"))
+
+ # Find .env but don't search above a specific directory
+ env_path = find_env_file(stop_at=Path("/path/to/project"))
+ """
+ current = start_dir or Path.cwd()
+ current = current.resolve()
+
+ stop_at = stop_at.resolve() if stop_at is not None else _find_project_root(current)
+
+ while True:
+ env_path = current / filename
+ if env_path.is_file():
+ return env_path
+
+ if stop_at is not None and current == stop_at:
+ return None
+
+ parent = current.parent
+ if parent == current:
+ # We've reached the filesystem root
+ return None
+
+ current = parent
+
+
class NotificationSettings(BaseSettings):
"""Notification-related settings."""
@@ -308,16 +388,17 @@ class MCPSettings(BaseSettings):
def from_env(cls) -> "MCPSettings":
"""Create settings from environment variables.
- Automatically loads .env file from current directory if it exists,
- then creates settings from the combined environment.
+ Automatically discovers and loads .env file by traversing upward from
+ the current directory through parent directories until a .env file is
+ found or the filesystem root is reached.
The .env file is loaded with override=False, meaning existing
- environment variables take precedence. Multiple calls are safe
+ environment variables take precedence. Multiple calls are safe.
"""
from dotenv import load_dotenv
- env_path = Path.cwd() / ".env"
- if env_path.exists():
+ env_path = find_env_file()
+ if env_path is not None:
load_dotenv(env_path, override=False)
return cls()
diff --git a/libs/arcade-mcp-server/pyproject.toml b/libs/arcade-mcp-server/pyproject.toml
index 5facba4c..132dfa1f 100644
--- a/libs/arcade-mcp-server/pyproject.toml
+++ b/libs/arcade-mcp-server/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "arcade-mcp-server"
-version = "1.17.3"
+version = "1.17.4"
description = "Model Context Protocol (MCP) server framework for Arcade.dev"
readme = "README.md"
authors = [{ name = "Arcade.dev" }]
diff --git a/libs/tests/arcade_mcp_server/test_env_discovery.py b/libs/tests/arcade_mcp_server/test_env_discovery.py
new file mode 100644
index 00000000..e45ab33b
--- /dev/null
+++ b/libs/tests/arcade_mcp_server/test_env_discovery.py
@@ -0,0 +1,111 @@
+"""Tests for find_env_file() upward directory traversal."""
+
+from pathlib import Path
+
+import pytest
+from arcade_mcp_server.settings import find_env_file
+
+
+class TestFindEnvFile:
+ """Test the find_env_file() utility function."""
+
+ def test_finds_env_in_current_directory(
+ self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
+ """Should find .env file in cwd."""
+ env_file = tmp_path / ".env"
+ env_file.write_text("TEST_VAR=value")
+ monkeypatch.chdir(tmp_path)
+
+ assert find_env_file() == env_file
+
+ def test_finds_env_in_parent_directory(
+ self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
+ """Should traverse upward to find .env in parent (no pyproject.toml boundary)."""
+ subdir = tmp_path / "a" / "b" / "c"
+ subdir.mkdir(parents=True)
+ env_file = tmp_path / ".env"
+ env_file.write_text("TEST_VAR=value")
+ monkeypatch.chdir(subdir)
+
+ assert find_env_file() == env_file
+
+ def test_prefers_closest_env_file(
+ self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
+ """Should find closest .env when multiple exist."""
+ subdir = tmp_path / "subdir"
+ subdir.mkdir()
+ (tmp_path / ".env").write_text("ROOT=1")
+ closer_env = subdir / ".env"
+ closer_env.write_text("CLOSER=1")
+ monkeypatch.chdir(subdir)
+
+ assert find_env_file() == closer_env
+
+ def test_returns_none_when_not_found(
+ self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
+ """Should return None when no .env exists."""
+ subdir = tmp_path / "subdir"
+ subdir.mkdir()
+ monkeypatch.chdir(subdir)
+
+ assert find_env_file() is None
+
+ def test_stop_at_limits_traversal(
+ self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
+ """Explicit stop_at should prevent traversing past specified directory."""
+ project = tmp_path / "project" / "src"
+ project.mkdir(parents=True)
+ (tmp_path / ".env").write_text("OUTSIDE=1")
+ monkeypatch.chdir(project)
+
+ assert find_env_file(stop_at=tmp_path / "project") is None
+
+ def test_stops_at_pyproject_toml_boundary(
+ self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
+ """Should not traverse past directory containing pyproject.toml."""
+ project_root = tmp_path / "project"
+ project_root.mkdir()
+ (project_root / "pyproject.toml").write_text("[project]\nname = 'test'")
+ src = project_root / "src"
+ src.mkdir()
+ # .env is above the project root — should NOT be found
+ (tmp_path / ".env").write_text("OUTSIDE=1")
+ monkeypatch.chdir(src)
+
+ assert find_env_file() is None
+
+ def test_finds_env_at_pyproject_toml_level(
+ self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
+ """Should find .env at the same level as pyproject.toml."""
+ project_root = tmp_path / "project"
+ project_root.mkdir()
+ (project_root / "pyproject.toml").write_text("[project]\nname = 'test'")
+ env_file = project_root / ".env"
+ env_file.write_text("SECRET=value")
+ src = project_root / "src"
+ src.mkdir()
+ monkeypatch.chdir(src)
+
+ assert find_env_file() == env_file
+
+ def test_finds_env_below_pyproject_toml(
+ self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
+ """Should find .env in a subdirectory within the project."""
+ project_root = tmp_path / "project"
+ project_root.mkdir()
+ (project_root / "pyproject.toml").write_text("[project]\nname = 'test'")
+ src = project_root / "src"
+ src.mkdir()
+ env_file = src / ".env"
+ env_file.write_text("SECRET=value")
+ monkeypatch.chdir(src)
+
+ assert find_env_file() == env_file
diff --git a/libs/tests/cli/test_configure.py b/libs/tests/cli/test_configure.py
index ac7b430d..b2d9e3de 100644
--- a/libs/tests/cli/test_configure.py
+++ b/libs/tests/cli/test_configure.py
@@ -1,3 +1,5 @@
+"""Tests for get_tool_secrets() in arcade configure."""
+
import json
import sys
import types
@@ -10,6 +12,7 @@ from arcade_cli.configure import (
_resolve_windows_appdata,
_warn_overwrite,
configure_client,
+ get_tool_secrets,
)
@@ -30,6 +33,28 @@ def _assert_stdio_entry(entry: dict) -> None:
assert "env" in entry
+def test_get_tool_secrets_loads_from_env_file(
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+) -> None:
+ """Should load secrets from .env file."""
+ env_file = tmp_path / ".env"
+ env_file.write_text("SECRET_ONE=value1\nSECRET_TWO=value2")
+ monkeypatch.chdir(tmp_path)
+
+ secrets = get_tool_secrets()
+ assert secrets.get("SECRET_ONE") == "value1"
+ assert secrets.get("SECRET_TWO") == "value2"
+
+
+def test_get_tool_secrets_returns_empty_when_no_env(
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+) -> None:
+ """Should return empty dict when no .env exists."""
+ monkeypatch.chdir(tmp_path)
+
+ assert get_tool_secrets() == {}
+
+
# ---------------------------------------------------------------------------
# _format_path_for_display()
# ---------------------------------------------------------------------------
diff --git a/libs/tests/cli/test_new_cli.py b/libs/tests/cli/test_new_cli.py
index 0702d2de..4b72b711 100644
--- a/libs/tests/cli/test_new_cli.py
+++ b/libs/tests/cli/test_new_cli.py
@@ -15,7 +15,7 @@ def test_create_new_toolkit_minimal_with_spaces(tmp_path: Path) -> None:
server_root = output_dir / "my_server"
assert (server_root / "pyproject.toml").is_file()
assert (server_root / "src" / "my_server" / "server.py").is_file()
- assert (server_root / "src" / "my_server" / ".env.example").is_file()
+ assert (server_root / ".env.example").is_file()
def test_create_new_toolkit_minimal_prints_next_steps(tmp_path: Path) -> None:
diff --git a/libs/tests/conftest.py b/libs/tests/conftest.py
index 4adf1086..95be27dd 100644
--- a/libs/tests/conftest.py
+++ b/libs/tests/conftest.py
@@ -41,21 +41,23 @@ def pytest_collection_modifyitems(config, items):
@pytest.fixture(autouse=True)
-def disable_usage_tracking():
- """Disable CLI usage tracking for all tests.
+def isolate_environment():
+ """Isolate environment variables for each test.
- This prevents test runs from sending analytics events to PostHog.
- The fixture is autouse=True so it applies automatically to every test.
+ This fixture captures the entire environment before a test and restores it
+ after. This ensures that environment variables set by load_dotenv() or any
+ other mechanism during tests don't leak into subsequent tests.
+
+ This also disables CLI usage tracking to prevent test runs from sending
+ analytics events to PostHog.
"""
- original_value = os.environ.get("ARCADE_USAGE_TRACKING")
+ original_env = os.environ.copy()
# Disable tracking
os.environ["ARCADE_USAGE_TRACKING"] = "0"
yield
- # Restore original value after test
- if original_value is None:
- os.environ.pop("ARCADE_USAGE_TRACKING", None)
- else:
- os.environ["ARCADE_USAGE_TRACKING"] = original_value
+ # Restore the original environment
+ os.environ.clear()
+ os.environ.update(original_env)
diff --git a/pyproject.toml b/pyproject.toml
index 11eb5abf..9620babf 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "arcade-mcp"
-version = "1.11.1"
+version = "1.11.2"
description = "Arcade.dev - Tool Calling platform for Agents"
readme = "README.md"
license = { file = "LICENSE" }
@@ -19,7 +19,7 @@ requires-python = ">=3.10"
dependencies = [
# CLI dependencies
- "arcade-mcp-server>=1.17.2,<2.0.0",
+ "arcade-mcp-server>=1.17.4,<2.0.0",
"arcade-core>=4.4.2,<5.0.0",
"typer==0.10.0",
"rich>=14.0.0,<15.0.0",
diff --git a/tests/integration/no_auth_cli_smoke.py b/tests/integration/no_auth_cli_smoke.py
index d828f003..d674a53d 100644
--- a/tests/integration/no_auth_cli_smoke.py
+++ b/tests/integration/no_auth_cli_smoke.py
@@ -255,7 +255,7 @@ def _run_scaffold_and_protocol_smoke(repo_root: Path) -> None:
generated_root = scaffold_dir / "my_server"
_ensure_exists(generated_root / "pyproject.toml")
_ensure_exists(generated_root / "src" / "my_server" / "server.py")
- _ensure_exists(generated_root / "src" / "my_server" / ".env.example")
+ _ensure_exists(generated_root / ".env.example")
generated_pyproject = generated_root / "pyproject.toml"
_add_local_uv_sources(generated_pyproject, repo_root)