From c866620435e7a100b507da77c5378b0ff143fbfb Mon Sep 17 00:00:00 2001 From: Francisco Or Something Date: Thu, 30 Apr 2026 20:03:53 -0300 Subject: [PATCH] fix(arcade-mcp-server): report missing debug stacktraces (#836) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Return an explicit `[DEBUG] stacktrace: unavailable ...` note when the stacktrace debug flag is enabled but the tool error payload has no stacktrace. - Preserve existing behavior for real stacktraces and for developer messages, including not leaking developer details unless the developer-message flag is enabled. - Clarify the toolkit-author docs around when stacktraces exist, such as unhandled exceptions or chained `raise ... from exc` errors. ## Test plan - `pre-commit run --files CLAUDE.md libs/arcade-mcp-server/arcade_mcp_server/_debug_exposure.py libs/tests/arcade_mcp_server/test_debug_exposure.py libs/tests/arcade_mcp_server/test_debug_exposure_integration.py` - `uv run --with pytest --with pytest-asyncio --with pytest-cov pytest libs/tests/arcade_mcp_server/test_debug_exposure.py libs/tests/arcade_mcp_server/test_debug_exposure_integration.py -v` - `ruff format --check libs/arcade-mcp-server/arcade_mcp_server/_debug_exposure.py libs/tests/arcade_mcp_server/test_debug_exposure.py libs/tests/arcade_mcp_server/test_debug_exposure_integration.py` - `ruff check libs/arcade-mcp-server/arcade_mcp_server/_debug_exposure.py libs/tests/arcade_mcp_server/test_debug_exposure.py libs/tests/arcade_mcp_server/test_debug_exposure_integration.py` --- > [!NOTE] > **Low Risk** > Low risk: changes are limited to debug-only error-message augmentation when an explicit env flag is enabled; default runtime behavior is unchanged. Main risk is only in local debugging scenarios where the new note could affect log parsing or expected error text. > > **Overview** > When `ARCADE_DEBUG_EXPOSE_STACKTRACE_IN_TOOL_ERROR_RESPONSES` is enabled, tool error messages now **always include a stacktrace debug section**: either the actual stacktrace (when present) or an explicit `[DEBUG] stacktrace: unavailable ...` note when the tool error payload had no stacktrace. > > Adds/updates unit + integration coverage for the missing-stacktrace case and adjusts expectations around “flag enabled but no content.” Updates toolkit-author docs to clarify when stacktraces exist, and bumps `arcade-mcp-server` patch version to `1.21.2`. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 7d85196a30d8d29be98ffb252a13ef2a78057742. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --- CLAUDE.md | 6 ++++++ .../arcade_mcp_server/_debug_exposure.py | 7 +++++-- libs/arcade-mcp-server/pyproject.toml | 2 +- .../arcade_mcp_server/test_debug_exposure.py | 19 +++++++++++++++++-- .../test_debug_exposure_integration.py | 16 ++++++++++++++++ 5 files changed, 45 insertions(+), 5 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3d5b9785..f58aa3a5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -262,6 +262,12 @@ When set, these flags append `developer_message` and/or the tool stacktrace to t | `ARCADE_DEBUG_EXPOSE_DEVELOPER_MESSAGE_IN_TOOL_ERROR_RESPONSES` | Appends `developer_message` to the error response `message` field | | `ARCADE_DEBUG_EXPOSE_STACKTRACE_IN_TOOL_ERROR_RESPONSES` | Appends the tool stacktrace to the error response `message` field | +The stacktrace flag does not create a traceback if the tool error payload has no stacktrace. It +appends an existing `stacktrace` value when present; otherwise it appends a debug note saying the +stacktrace is unavailable. For example, unhandled exceptions and `ToolRuntimeError`/`FatalToolError` +raised with a chained cause (`raise ... from exc`) have one, while directly raised +`FatalToolError(...)` values usually do not. + **Never enable in production.** The `message` field is returned verbatim to whoever called the tool — LLMs, transcripts, end-user UIs, and anything else downstream. ## Project Layout diff --git a/libs/arcade-mcp-server/arcade_mcp_server/_debug_exposure.py b/libs/arcade-mcp-server/arcade_mcp_server/_debug_exposure.py index b4939bac..d27704eb 100644 --- a/libs/arcade-mcp-server/arcade_mcp_server/_debug_exposure.py +++ b/libs/arcade-mcp-server/arcade_mcp_server/_debug_exposure.py @@ -77,8 +77,11 @@ def augment_error_message_for_debug( extras: list[str] = [] if developer_message and _leak_enabled(_ENV_EXPOSE_DEVELOPER_MESSAGE): extras.append(f"developer_message: {developer_message}") - if stacktrace and _leak_enabled(_ENV_EXPOSE_STACKTRACE): - extras.append(f"stacktrace:\n{stacktrace}") + if _leak_enabled(_ENV_EXPOSE_STACKTRACE): + if stacktrace: + extras.append(f"stacktrace:\n{stacktrace}") + else: + extras.append("stacktrace: unavailable (tool error payload did not include one)") if not extras: return message return f"{message}\n\n[DEBUG] " + "\n\n[DEBUG] ".join(extras) diff --git a/libs/arcade-mcp-server/pyproject.toml b/libs/arcade-mcp-server/pyproject.toml index f74ce5ae..1bef8723 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.21.1" +version = "1.21.2" 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_debug_exposure.py b/libs/tests/arcade_mcp_server/test_debug_exposure.py index bdb751b4..7937211e 100644 --- a/libs/tests/arcade_mcp_server/test_debug_exposure.py +++ b/libs/tests/arcade_mcp_server/test_debug_exposure.py @@ -93,6 +93,20 @@ def test_stacktrace_flag_enabled(monkeypatch): assert "secret internals" not in out +def test_stacktrace_flag_enabled_but_stacktrace_missing(monkeypatch): + monkeypatch.setenv(_ENV_STACKTRACE, _LEAK_MAGIC) + out = augment_error_message_for_debug( + "public error", + developer_message="secret internals", + stacktrace=None, + ) + assert "public error" in out + assert "[DEBUG] stacktrace: unavailable" in out + assert "tool error payload did not include one" in out + # Developer-message flag off → dev message must NOT leak. + assert "secret internals" not in out + + def test_both_flags_enabled(monkeypatch): monkeypatch.setenv(_ENV_DEV_MSG, _LEAK_MAGIC) monkeypatch.setenv(_ENV_STACKTRACE, _LEAK_MAGIC) @@ -104,11 +118,12 @@ def test_both_flags_enabled(monkeypatch): def test_flag_enabled_but_no_content_to_leak(monkeypatch): - """Flag on but developer_message/stacktrace are None → message unchanged.""" + """Developer flag on but developer_message None → only stacktrace absence is reported.""" monkeypatch.setenv(_ENV_DEV_MSG, _LEAK_MAGIC) monkeypatch.setenv(_ENV_STACKTRACE, _LEAK_MAGIC) out = augment_error_message_for_debug("public error", None, None) - assert out == "public error" + assert "[DEBUG] developer_message:" not in out + assert "[DEBUG] stacktrace: unavailable" in out def test_activation_warning_emitted_once_per_process(monkeypatch, caplog): diff --git a/libs/tests/arcade_mcp_server/test_debug_exposure_integration.py b/libs/tests/arcade_mcp_server/test_debug_exposure_integration.py index 0e67976a..05e5a3cb 100644 --- a/libs/tests/arcade_mcp_server/test_debug_exposure_integration.py +++ b/libs/tests/arcade_mcp_server/test_debug_exposure_integration.py @@ -271,6 +271,22 @@ async def test_integration_stacktrace_flag_leaks_traceback_through_mcp( assert "unexpected crash for query='ping'" in text +@pytest.mark.asyncio +async def test_integration_stacktrace_flag_reports_missing_traceback(erroring_server, monkeypatch): + """Directly raised ToolRuntimeError values may not carry a stacktrace. + + When the stacktrace flag is enabled, the MCP response should say that + explicitly instead of silently omitting the stacktrace debug section. + """ + monkeypatch.setenv(_ENV_STACKTRACE, _LEAK_MAGIC) + result = await _call(erroring_server, "raises_fatal_tool_error") + text = result.content[0].text + assert "Failed to fetch results" in text + assert "[DEBUG] stacktrace: unavailable" in text + assert "tool error payload did not include one" in text + assert "HTTP 503" not in text + + @pytest.mark.asyncio async def test_integration_both_flags_leak_through_mcp(erroring_server, monkeypatch): """Both flags together on an unhandled exception: developer_message (from