Skip to content

feat: redact secrets from tool results by default - #170

Merged
ChiragAgg5k merged 4 commits into
mainfrom
feat/redact-secrets
Nov 12, 2026
Merged

ChiragAgg5k merged 4 commits into
mainfrom
feat/redact-secrets

Conversation

@ChiragAgg5k

@ChiragAgg5k ChiragAgg5k commented Nov 12, 2026 •

Copy link
Copy Markdown
Member

What does this PR do?

Stops the MCP server from putting secrets into the model's context. Every response field Appwrite's API spec marks format: password is redacted from tool results by default and replaced with a fingerprint. A new reveal_secrets option returns the values when the user asks to see them.

The problem

Whatever a tool returns goes into the model's context, then into the client's local transcripts, subagent transcripts and tool-result caches. Some Appwrite responses carry live secrets, and the MCP returned them in full:

Tool Field
organization_create_ephemeral_project_key secret, on every call
Key update, even a scope-only change the long-lived secret
organization_create_project_key secret
webhooks_create, webhooks_update_secret the signing secret
users_list, users_get password (the argon2 hash), for every user

As a result, a key created or rotated through the MCP was leaked the moment it existed, so an agent couldn't rotate a leaked key without leaking the replacement. The CLI already hides these by default, so the two surfaces behaved differently on the same API.

Where the list of secret fields comes from

There is no hand-kept list in this repo. Secret fields are marked once in Appwrite's API spec with OpenAPI's standard format: password, and both this server and the CLI read that marking:

  1. feat: mark secret response fields with format password appwrite#15543 and #15547 mark 73 secret fields in server-ce. That covers key, session, token and device secrets, JWTs, MFA secrets and recovery codes, password hashes, provider tokens and every OAuth client secret.
  2. appwrite-labs/cloud#7502 marks 20 more on cloud-only models. That covers app keys and secrets, dedicated database passwords and connection strings, OAuth2 server tokens, device codes, domain-transfer codes and Stripe client secrets.
  3. feat: redact the response fields the spec marks format password sdk-generator#2219 (released in 5.19.0) makes the generator use the marking:
    • The CLI masks marked fields.
    • Python models carry the marking on each field: Field(..., json_schema_extra={'format': 'password'}).
  4. appwrite-console 0.16.0 (feat: Console Python SDK update for version 0.16.0 sdk-for-console-python#25) ships it, with 88 marked fields in the console SDK.

How it works

New module, redaction.py. After the SDK call, execute_registered_tool passes the typed result to redact() before it's serialised. redact() walks the pydantic model tree and handles each marked field like this:

Field value Result
A string Replaced with "[redacted]", plus a sibling <field>Fingerprint: sha256: and the first 12 hex characters of its SHA-256. The agent can tell which key it created, or check a stored copy matches, without seeing it
A non-empty list (recovery codes) Replaced with "[redacted]"
Empty or null Kept. It only says the secret is unset, or that Appwrite already hid it

Fields that aren't marked are serialised exactly as before (the same model_dump(by_alias=True, mode="json", exclude_unset=True) the SDK's to_dict uses).

A note to the model. When anything was redacted, a second text item names the fields. It also tells the model to read the value again with reveal_secrets=true only if the user explicitly asks, and never to repeat a create, which would make a new secret.

reveal_secrets. It's a boolean on appwrite_call_tool and appwrite_call_write_tool, off by default, and it's stripped from the arguments sent to Appwrite. A write that reveals still needs confirm_write and a reason, and the reason is recorded on the project's audit log as before.

Where it applies.

  • Every hidden tool call, whichever transport (hosted HTTP or stdio) and whichever auth (OAuth or API key).
  • Results stored as MCP resources, since redaction happens before storage.
  • The MCP Apps views (appwrite_show).

views._json now parses only the first text item, so the note never breaks a view.

What this affects

Area Effect
Tool results Marked secrets become [redacted] and a fingerprint. Everything else is unchanged
users_list and users_get password comes back as [redacted] with a fingerprint; $argon2… never appears
Key and webhook create, update and rotate secret is redacted unless reveal_secrets is set
Tool schemas appwrite_call_tool and appwrite_call_write_tool gain the optional reveal_secrets property
select queries Not redacted. They return rows untyped, the caller's own data rather than Appwrite's secrets
Responses the SDK can't parse The raw payload in the error is redacted by name: every field name an SDK model marks secret (31 names)
appwrite-console 0.12.0 → 0.16.0, which also adds 36 catalog methods: 1065 hosted, 720 for API keys. The docs and the flow diagram (SVG and the re-rendered PNG) are updated to match
requests and urllib3 2.34.2 and 2.8.0, the new SDK's minimum versions

Upgrading from 0.12 needed one test change: the SDK now fills defaults on partial documents. The select test's second half asserted that full models reject partial rows, which no longer holds. The part that matters is still asserted: a select result comes back exactly as Appwrite sent it, with no defaults filled in.

Test Plan

  • Existing e2e flows extended:
    • test_tools.py, test_reads_and_writes_run_only_through_their_own_tool:
      • webhooks_create's result doesn't contain the stored signing key anywhere.
      • secret is [redacted], and secretFingerprint equals the stored key's SHA-256 prefix.
      • The note names secret.
      • webhooks_update_secret with reveal_secrets: true returns the rotated key in full, with no note.
    • test_server.py, test_format_appwrite_error_returns_the_unhydrated_payload: a key secret in an unparsed payload comes back as [redacted] with the note.
    • test_stdio.py, test_acts_in_the_configured_environment: users_list over stdio contains no $argon2. Every user's password is [redacted] with a sha256: fingerprint.
  • ruff check, black --check and pyright (0 errors) pass.
  • Unit tests pass: 301.
  • Full e2e suite passes: 64.
  • Docker build: not run locally because the disk is full, so the docker job checks it.
  • Re-rendered docs/appwrite-mcp-flow.png from the SVG and compared it with the previous PNG: identical apart from the catalog line (38 services · 981 tools → 41 · 1065).

API key secrets, webhook signing keys, password hashes and other secrets went into the model's context, transcripts and caches the moment a call returned them. Redact every field Appwrite's spec marks format: password, read from the Console SDK model, leaving a fingerprint and a note; reveal_secrets returns them in full when the user asks.
0.16.0 carries format: password on every secret response field, which the redaction reads. It also adds 36 catalog methods (1065 hosted, 720 for API keys), so the documented counts and the flow diagram move with it.
@appwrite

appwrite Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

🟢 Tier S · Ready to merge

Adds default redaction and SHA-256 fingerprints for API-spec password-format fields in Appwrite tool results, with an opt-in reveal_secrets argument. The redaction applies to typed results and SDK parse-error payloads, while result views, resources, and tool guidance handle redacted responses. The pull request also updates the SDK dependency, catalog counts, tests, documentation, and flow diagrams; the latest commits make parse-error payloads honor the reveal option.

Latest changes: The latest commits pass reveal_secrets through Appwrite error formatting so parse-error payloads are redacted by default but can return the raw secret when explicitly requested, and add a unit test for that behavior.

Verdict New comments Fixed Still open
✅ Approved 0 0 0
📂 Walkthrough · 14
File Change
AGENTS.md Adds the redaction module to the source layout documentation.
docs/appwrite-mcp-flow.png Refreshes the flow diagram image for the expanded catalog.
docs/appwrite-mcp-flow.svg Refreshes the source flow diagram for the expanded catalog.
docs/self-hosted.md Updates the API-key catalog method count.
docs/tool-surface.md Documents redaction and reveal behavior and updates SDK and catalog details.
pyproject.toml Raises the appwrite-console dependency version range.
src/mcp_server_appwrite/apps/views.py Parses the first result text item so redaction notes do not break views.
src/mcp_server_appwrite/operator.py Adds reveal_secrets to operator tool calls and excludes it from Appwrite arguments.
src/mcp_server_appwrite/redaction.py Redacts password-format typed fields and named secret fields in unhydrated payloads.
src/mcp_server_appwrite/server.py Applies result redaction and notes, and now passes the reveal option through parse-error formatting.
tests/e2e/test_stdio.py Checks that user password hashes are redacted in stdio results.
tests/e2e/test_tools.py Covers webhook secret redaction, fingerprints, notes, and explicit reveal behavior.
tests/unit/test_server.py Updates SDK catalog expectations and tests redaction and reveal behavior for parse-error payloads.
uv.lock Updates the locked SDK and related dependency versions.

Reviewed the commits since 7412d95 · Details · Comment @appwrite review to re-run, or mention @appwrite with a question.

appwrite[bot]
appwrite Bot previously requested changes Nov 12, 2026

@appwrite appwrite Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Tier B · 1 blocking finding to address. Summary

Comment thread src/mcp_server_appwrite/server.py Resolved
A response the SDK fails to hydrate is returned raw, without the model's field types, so secrets in it went through unredacted. Redact it by name: every field name an SDK model marks format: password.
@appwrite
appwrite Bot dismissed their stale review November 12, 2026 15:52

The findings that requested changes are resolved. See the summary comment for what is still open.

@appwrite appwrite Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Tier A · See the inline comments. Summary

Comment thread src/mcp_server_appwrite/server.py Resolved
A create or rotate whose response the SDK fails to hydrate returns the raw payload, and redacting it regardless of reveal_secrets could leave a one-time secret unrecoverable.

@appwrite appwrite Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Tier S · Looks good to merge. Summary

@ChiragAgg5k
ChiragAgg5k merged commit 96d0278 into main Nov 12, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant