From 0a8a4b9ac109438de6f98536c39fd3e804a6d706 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 12 Nov 2026 18:04:51 +0000 Subject: [PATCH] fix: keep ordinary fields that share a name with a secret The parse-error fallback redacts by field name. 'code' is the MFA challenge code but also a country's or language's code, so it hid those too. Leave out names that are ordinary strings on another model, as the CLI now does (appwrite/sdk-generator#2222). --- src/mcp_server_appwrite/redaction.py | 22 +++++++++++++++++----- tests/unit/test_server.py | 6 ++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/mcp_server_appwrite/redaction.py b/src/mcp_server_appwrite/redaction.py index 4c35901..ae95f48 100644 --- a/src/mcp_server_appwrite/redaction.py +++ b/src/mcp_server_appwrite/redaction.py @@ -21,7 +21,7 @@ import hashlib from functools import cache -from typing import Any +from typing import Any, get_args import appwrite_console.models from pydantic import BaseModel @@ -116,14 +116,26 @@ def _mask(data: dict[str, Any], key: str, fields: set[str]) -> None: @cache def _secret_names() -> frozenset[str]: - """Every field name an SDK model marks secret.""" - names: set[str] = set() + """The field names SDK models mark secret, less any that is an ordinary + string on another model: ``code`` is the MFA challenge code, but also a + country's code, and matched by name it would hide both.""" + secret: set[str] = set() + plain: set[str] = set() for model in vars(appwrite_console.models).values(): if isinstance(model, type) and issubclass(model, BaseModel): for name, field in model.model_fields.items(): if _secret(field): - names.add(field.alias or name) - return frozenset(names) + secret.add(field.alias or name) + elif _text(field.annotation): + plain.add(field.alias or name) + return frozenset(secret - plain) + + +def _text(annotation: Any) -> bool: + """A string, or a list or optional of one.""" + if annotation is str: + return True + return any(_text(argument) for argument in get_args(annotation)) def _secret(field: FieldInfo) -> bool: diff --git a/tests/unit/test_server.py b/tests/unit/test_server.py index 6e993f6..9cfcb44 100644 --- a/tests/unit/test_server.py +++ b/tests/unit/test_server.py @@ -895,6 +895,12 @@ def test_format_appwrite_error_returns_the_unhydrated_payload(self): self.assertNotIn("standard_0123456789abcdef", message) self.assertIn('"secret": "[redacted]"', message) self.assertIn("Secrets redacted from this result: secret.", message) + # A name that is an ordinary field elsewhere (a country's code) stays. + coded = AppwriteException( + "Unable to parse response into Country: 1 validation error", + response={"name": "Germany", "code": "DE"}, + ) + self.assertIn('"code": "DE"', _format_appwrite_error(coded)) # Asked for, a one-time secret is not lost to a parse failure. revealed = _format_appwrite_error(exc, tool_name="projects_create", reveal=True)