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:
Francisco Or Something 2026-04-30 20:03:53 -03:00 committed by GitHub
parent dc4607daa4
commit c866620435
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 45 additions and 5 deletions

View file

@ -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

View file

@ -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)

View file

@ -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" }]

View file

@ -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):

View file

@ -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