## 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 -->
87 lines
3.4 KiB
Python
87 lines
3.4 KiB
Python
"""
|
|
Debug-only escape hatch for MCP tool error responses.
|
|
|
|
MCP clients typically render only the ``message`` field of a tool error
|
|
response, dropping ``developer_message`` and ``stacktrace``. That makes
|
|
server-side iteration painful when a tool is failing. The flags in this
|
|
module let a toolkit author opt in to appending those internals to the
|
|
``message`` field while debugging.
|
|
|
|
DEBUG-ONLY. Activating these flags can leak paths, tokens, or PII to
|
|
callers. Don't add more flags of this shape — put debug info in logs
|
|
instead.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
# Acknowledgement string a developer must set as the env value. Picked to be
|
|
# impossible to set by mistake — no sane config management or CI will ever
|
|
# emit this string.
|
|
_DEBUG_LEAK_MAGIC = "yes-i-accept-leaking-internals-to-the-agent"
|
|
|
|
_ENV_EXPOSE_DEVELOPER_MESSAGE = "ARCADE_DEBUG_EXPOSE_DEVELOPER_MESSAGE_IN_TOOL_ERROR_RESPONSES"
|
|
_ENV_EXPOSE_STACKTRACE = "ARCADE_DEBUG_EXPOSE_STACKTRACE_IN_TOOL_ERROR_RESPONSES"
|
|
|
|
# One-shot warning state per flag. The rejection warning (truthy but not the
|
|
# magic string) and the activation warning (magic string set) are tracked in
|
|
# *separate* sets so that fixing a misconfigured flag within the same process
|
|
# still fires the critical activation warning.
|
|
_warned_rejected: set[str] = set()
|
|
_warned_activated: set[str] = set()
|
|
|
|
|
|
def _leak_enabled(env_var: str) -> bool:
|
|
raw = os.environ.get(env_var)
|
|
if raw is None:
|
|
return False
|
|
if raw.strip() != _DEBUG_LEAK_MAGIC:
|
|
# A value is set but it isn't the magic ack. Treat as off and, if it
|
|
# looks like someone tried a boolean, nudge them via a log so the
|
|
# silence isn't confusing.
|
|
if raw.strip().lower() in {"1", "true", "yes", "on"} and env_var not in _warned_rejected:
|
|
_warned_rejected.add(env_var)
|
|
_logger.warning(
|
|
"%s is set to a truthy value but not to the required "
|
|
"acknowledgement string. Flag remains OFF. "
|
|
"See arcade_mcp_server/_debug_exposure.py.",
|
|
env_var,
|
|
)
|
|
return False
|
|
if env_var not in _warned_activated:
|
|
_warned_activated.add(env_var)
|
|
_logger.warning(
|
|
"%s is ENABLED. Tool error internals will be appended to the "
|
|
"`message` field of MCP tool error responses. This can leak paths, "
|
|
"tokens, or PII to callers. DO NOT USE IN PRODUCTION.",
|
|
env_var,
|
|
)
|
|
return True
|
|
|
|
|
|
def augment_error_message_for_debug(
|
|
message: str,
|
|
developer_message: str | None,
|
|
stacktrace: str | None,
|
|
) -> str:
|
|
"""Append debug internals to ``message`` when the corresponding env flags are set.
|
|
|
|
This is a no-op in the default case (both flags off), and also a no-op when
|
|
the flags are set to anything other than the activation ack string. See
|
|
module docstring for the full rationale.
|
|
"""
|
|
extras: list[str] = []
|
|
if developer_message and _leak_enabled(_ENV_EXPOSE_DEVELOPER_MESSAGE):
|
|
extras.append(f"developer_message: {developer_message}")
|
|
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)
|