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