fix(arcade-mcp-server): report missing debug stacktraces (#836)
## 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` <!-- CURSOR_SUMMARY --> --- > [!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`. > > <sup>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).</sup> <!-- /CURSOR_SUMMARY -->
This commit is contained in:
parent
dc4607daa4
commit
c866620435
5 changed files with 45 additions and 5 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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" }]
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue