mcp-server-appwrite 2.2.3__tar.gz → 2.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/.env.example +5 -0
  2. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/AGENTS.md +7 -4
  3. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/PKG-INFO +12 -1
  4. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/README.md +11 -0
  5. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/docs/apps.md +1 -1
  6. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/docs/self-hosted.md +25 -0
  7. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/docs/tool-surface.md +17 -3
  8. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/apps/views.py +54 -17
  9. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/assets/instructions.json +4 -2
  10. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/assets/view.html +5 -1
  11. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/context.py +73 -18
  12. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/data/docs_index.npz +1 -1
  13. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/error_classification.py +7 -0
  14. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/error_monitoring.py +3 -0
  15. mcp_server_appwrite-2.3.0/src/mcp_server_appwrite/headers.py +104 -0
  16. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/operator.py +99 -10
  17. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/server.py +69 -7
  18. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/e2e/support.py +46 -1
  19. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/e2e/test_apps.py +35 -0
  20. mcp_server_appwrite-2.3.0/tests/e2e/test_stdio.py +180 -0
  21. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/e2e/test_tools.py +179 -2
  22. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/unit/test_operator.py +1 -0
  23. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/unit/test_server.py +4 -0
  24. mcp_server_appwrite-2.2.3/tests/e2e/test_stdio.py +0 -100
  25. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/.dockerignore +0 -0
  26. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/.github/workflows/ci.yml +0 -0
  27. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/.github/workflows/production.yml +0 -0
  28. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/.github/workflows/publish.yml +0 -0
  29. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/.github/workflows/staging.yml +0 -0
  30. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/.github/workflows/update-docs-index.yml +0 -0
  31. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/.gitignore +0 -0
  32. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/.python-version +0 -0
  33. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/CLAUDE.md +0 -0
  34. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/Dockerfile +0 -0
  35. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/LICENSE +0 -0
  36. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/compose.yaml +0 -0
  37. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/docs/appwrite-mcp-flow.png +0 -0
  38. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/docs/appwrite-mcp-flow.svg +0 -0
  39. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/docs/authentication.md +0 -0
  40. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/docs/development.md +0 -0
  41. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/docs/documentation-search.md +0 -0
  42. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/docs/events.md +0 -0
  43. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/docs/flags.md +0 -0
  44. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/docs/images/claude-desktop-local-mcp-servers.png +0 -0
  45. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/docs/images/codex-mcp.png +0 -0
  46. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/docs/images/cursor-mcp.png +0 -0
  47. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/glama.json +0 -0
  48. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/pyproject.toml +0 -0
  49. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/scripts/build_docs_index.py +0 -0
  50. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/scripts/docs_release_notes.py +0 -0
  51. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/scripts/render_server_json.py +0 -0
  52. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/server.template.json +0 -0
  53. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/__init__.py +0 -0
  54. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/__main__.py +0 -0
  55. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/apps/__init__.py +0 -0
  56. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/apps/executions.py +0 -0
  57. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/apps/files.py +0 -0
  58. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/apps/protocol.py +0 -0
  59. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/apps/resource.py +0 -0
  60. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/apps/usage.py +0 -0
  61. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/apps/users.py +0 -0
  62. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/assets/__init__.py +0 -0
  63. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/assets/favicon.svg +0 -0
  64. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/auth.py +0 -0
  65. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/catalog_policy.py +0 -0
  66. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/constants.py +0 -0
  67. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/docs.py +0 -0
  68. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/docs_index.py +0 -0
  69. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/docs_search.py +0 -0
  70. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/docs_source.py +0 -0
  71. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/events/__init__.py +0 -0
  72. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/events/bus.py +0 -0
  73. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/events/catalog.py +0 -0
  74. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/events/delivery.py +0 -0
  75. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/events/egress.py +0 -0
  76. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/events/envelope.py +0 -0
  77. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/events/errors.py +0 -0
  78. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/events/ingress.py +0 -0
  79. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/events/projection.py +0 -0
  80. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/events/protocol.py +0 -0
  81. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/events/streams.py +0 -0
  82. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/events/subscriptions.py +0 -0
  83. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/events/webhooks.py +0 -0
  84. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/flags.py +0 -0
  85. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/http_app.py +0 -0
  86. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/instructions.py +0 -0
  87. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/legacy.py +0 -0
  88. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/service.py +0 -0
  89. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/telemetry.py +0 -0
  90. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/src/mcp_server_appwrite/tool_manager.py +0 -0
  91. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/e2e/fixtures/appwrite/deployment_function_duplicate.json +0 -0
  92. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/e2e/fixtures/appwrite/deployment_function_failed.json +0 -0
  93. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/e2e/fixtures/appwrite/deployment_function_ready.json +0 -0
  94. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/e2e/fixtures/appwrite/deployment_site_ready.json +0 -0
  95. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/e2e/fixtures/appwrite/execution_async_failed.json +0 -0
  96. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/e2e/fixtures/appwrite/execution_async_waiting.json +0 -0
  97. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/e2e/fixtures/appwrite/execution_sync_failed.json +0 -0
  98. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/e2e/fixtures/appwrite/file_created.json +0 -0
  99. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/e2e/fixtures/appwrite/function_activation.json +0 -0
  100. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/e2e/fixtures/appwrite/row_created.json +0 -0
  101. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/e2e/fixtures/appwrite/user_created.json +0 -0
  102. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/e2e/test_events_ingress.py +0 -0
  103. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/e2e/test_events_protocol.py +0 -0
  104. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/e2e/test_events_stream.py +0 -0
  105. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/e2e/test_events_subscribe.py +0 -0
  106. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/integration/support.py +0 -0
  107. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/integration/test_avatars.py +0 -0
  108. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/integration/test_console_sdk_oauth.py +0 -0
  109. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/integration/test_docs_index.py +0 -0
  110. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/integration/test_docs_source.py +0 -0
  111. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/integration/test_functions.py +0 -0
  112. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/integration/test_locale.py +0 -0
  113. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/integration/test_messaging.py +0 -0
  114. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/integration/test_oauth_discovery.py +0 -0
  115. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/integration/test_operator.py +0 -0
  116. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/integration/test_sites.py +0 -0
  117. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/integration/test_storage.py +0 -0
  118. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/integration/test_tables_db.py +0 -0
  119. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/integration/test_teams.py +0 -0
  120. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/integration/test_users.py +0 -0
  121. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/test_all.py +0 -0
  122. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/unit/fixtures/jwe-v1.json +0 -0
  123. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/unit/test_auth.py +0 -0
  124. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/unit/test_context.py +0 -0
  125. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/unit/test_docs.py +0 -0
  126. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/unit/test_docs_search.py +0 -0
  127. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/unit/test_error_classification.py +0 -0
  128. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/unit/test_error_monitoring.py +0 -0
  129. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/unit/test_events_delivery.py +0 -0
  130. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/unit/test_events_egress.py +0 -0
  131. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/unit/test_events_envelope.py +0 -0
  132. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/unit/test_events_protocol.py +0 -0
  133. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/unit/test_http_app.py +0 -0
  134. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/unit/test_instructions.py +0 -0
  135. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/unit/test_release_metadata.py +0 -0
  136. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/unit/test_service.py +0 -0
  137. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/tests/unit/test_telemetry.py +0 -0
  138. {mcp_server_appwrite-2.2.3 → mcp_server_appwrite-2.3.0}/uv.lock +0 -0
@@ -32,6 +32,11 @@ PORT=8000
32
32
  # Generate a key with `openssl rand -base64 32`.
33
33
  # MCP_EVENTS_SEALING_KEYS=k1:<base64 key>
34
34
 
35
+ # stdio only. The project environment (Appwrite Cloud) to act in, with
36
+ # APPWRITE_PROJECT_ID set to the ID of the project it belongs to. Sent as
37
+ # X-Appwrite-Environment on every request.
38
+ # APPWRITE_ENVIRONMENT_ID=staging
39
+
35
40
  # Integration tests only (not used by the running server).
36
41
  # The live integration suite authenticates to the Appwrite API with an API key
37
42
  # and runs against APPWRITE_PROJECT_ID (above).
@@ -13,7 +13,8 @@ operator-style tools, supporting two deployments from one codebase:
13
13
  Appwrite REST API. Served primarily at `mcp.appwrite.io/`, with `/mcp` also
14
14
  available as a conventional alias.
15
15
  - **Self-hosted (`stdio`):** runs locally and authenticates with a project API
16
- key (`APPWRITE_PROJECT_ID`, `APPWRITE_API_KEY`, `APPWRITE_ENDPOINT`).
16
+ key (`APPWRITE_PROJECT_ID`, `APPWRITE_API_KEY`, `APPWRITE_ENDPOINT`, and
17
+ optionally `APPWRITE_ENVIRONMENT_ID` for a project environment).
17
18
 
18
19
  Python ≥ 3.12, packaged with `hatchling`, managed with `uv`.
19
20
 
@@ -31,7 +32,8 @@ Source lives in `src/mcp_server_appwrite/`:
31
32
  | `tool_manager.py` | Registry of all services and their generated tools. |
32
33
  | `operator.py` | The compact "operator" surface — `appwrite_search_tools`, `appwrite_call_tool` / `appwrite_call_write_tool`, tool annotations, result/resource storage, write confirmation. |
33
34
  | `instructions.py` | `serverInfo.description` and the server instructions sent on initialize, rendered from `assets/instructions.json` (a description plus ordered, transport/docs-conditional Markdown sections). Edit the JSON to change the wording; keep the `pyproject.toml` description in sync (a test enforces it). |
34
- | `context.py` | `appwrite_get_context` — workspace summary (project, services, account/org for OAuth). |
35
+ | `context.py` | `appwrite_get_context` — workspace summary (project, services, account/org for OAuth, a focused project's environments). |
36
+ | `headers.py` | Per-call request headers: `X-Appwrite-Environment` (a project environment, next to the project's ID) and `X-Appwrite-Reason` (why a write is made, validated and percent-encoded as Appwrite expects). |
35
37
  | `docs_search.py` | In-process semantic docs search (`appwrite_search_docs`) over a prebuilt index. |
36
38
  | `docs_source.py` | Fetches the published docs (`/docs/llms.txt` + per-page `.md` exports on appwrite.io) and chunks them for the index build. |
37
39
  | `docs_index.py` | Writes the index artifact deterministically and atomically: page and chunk hashes, reuse of cached chunk vectors, build lock, and the added/changed/removed page report. |
@@ -59,8 +61,9 @@ reads, `appwrite_call_write_tool` for everything else, and optionally
59
61
  `appwrite_search_docs`), while the full OAuth Appwrite catalog (41
60
62
  services) stays internal and is searched at runtime. API-key stdio uses a filtered
61
63
  27-service catalog. Mutating hidden tools run only through
62
- `appwrite_call_write_tool` and require `confirm_write=true`, so each exposed
63
- tool's annotations match every call it accepts. Large outputs are stored as MCP
64
+ `appwrite_call_write_tool` and require `confirm_write=true` and a one-line
65
+ `reason` (sent as `X-Appwrite-Reason`, recorded on the audit log), so each
66
+ exposed tool's annotations match every call it accepts. Large outputs are stored as MCP
64
67
  resources and returned as a preview + resource URI.
65
68
 
66
69
  With MCP Apps on (`flags.APPS`), hosts that render them also see
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: mcp-server-appwrite
3
- Version: 2.2.3
3
+ Version: 2.3.0
4
4
  Summary: Build and deploy application backends. Add authentication, PostgreSQL/MySQL databases, vector databases, storage, serverless functions, realtime, messaging, hosting, domains, CDN and firewall.
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -339,6 +339,17 @@ A browser window opens to authenticate on first connect.
339
339
 
340
340
  </details>
341
341
 
342
+ ## Reasons and environments
343
+
344
+ Every write the agent makes carries a short `reason` (why it made the change).
345
+ Appwrite records it on the project's audit log, so you can review what your
346
+ agent changed, and why, in the project's **Activity** tab.
347
+
348
+ On Appwrite Cloud, the agent can act in a project's environments (for example
349
+ `staging`): `appwrite_get_context` lists them, and each call takes the
350
+ environment's ID as `environment_id` next to the project's `project_id`. See
351
+ [Tool surface](docs/tool-surface.md#behavior).
352
+
342
353
  ## Self-hosted Appwrite
343
354
 
344
355
  Running your own Appwrite instance? Run the MCP server locally over `stdio` and
@@ -307,6 +307,17 @@ A browser window opens to authenticate on first connect.
307
307
 
308
308
  </details>
309
309
 
310
+ ## Reasons and environments
311
+
312
+ Every write the agent makes carries a short `reason` (why it made the change).
313
+ Appwrite records it on the project's audit log, so you can review what your
314
+ agent changed, and why, in the project's **Activity** tab.
315
+
316
+ On Appwrite Cloud, the agent can act in a project's environments (for example
317
+ `staging`): `appwrite_get_context` lists them, and each call takes the
318
+ environment's ID as `environment_id` next to the project's `project_id`. See
319
+ [Tool surface](docs/tool-surface.md#behavior).
320
+
310
321
  ## Self-hosted Appwrite
311
322
 
312
323
  Running your own Appwrite instance? Run the MCP server locally over `stdio` and
@@ -10,7 +10,7 @@ interactive view inside the chat. This server offers one view,
10
10
  | Name | Seen by | What it does |
11
11
  | --- | --- | --- |
12
12
  | `appwrite_show` | model and view | `view=table`: a page of a table's rows, newest first, with `cursor` paging. `view=deployment`: a Function or Site deployment (the latest unless `deployment_id` is given) with the last 200 build log lines. `view=executions`: a Function's executions, newest first, optionally one `status`, with `cursor` paging; `execution_id` also opens one with its request and response headers, status code, duration, and the last 200 lines of logs and of errors. `view=files`: a Storage bucket's files, newest first, with `search` (file name) and `cursor` paging; `file_id` also opens one. `view=users`: the project's Auth users, newest first, with `search` (name, email, phone or ID) and `cursor` paging; `user_id` also opens one with their ten newest sessions. `view=usage`: a project's requests, bandwidth (in and out) and executions per interval over `range` (`24h` hourly, `7d` or `30d` daily), with totals and the newest snapshot of its users, resource counts and storage. Read-only. |
13
- | `appwrite_view_write` | view only (`visibility: ["app"]`) | Runs one of `tables_db_update_row`, `functions_update_deployment_status`, `sites_update_deployment_status`, `users_update_status`, `users_update_labels`, `users_update_email_verification`, only with `confirm_write=true`. |
13
+ | `appwrite_view_write` | view only (`visibility: ["app"]`) | Runs one of `tables_db_update_row`, `functions_update_deployment_status`, `sites_update_deployment_status`, `users_update_status`, `users_update_labels`, `users_update_email_verification`, only with `confirm_write=true`. A `reason` is optional here (a person confirmed the change in the view) and sent as `X-Appwrite-Reason` when given. |
14
14
  | `ui://appwrite/view` | host | One HTML document (`assets/view.html`) that renders every view. |
15
15
 
16
16
  `appwrite_show` returns a text summary in `content`, which is what the model and
@@ -43,6 +43,31 @@ For a local Docker Appwrite instance the endpoint is typically
43
43
  `http://localhost:9501/v1` (HTTP, not HTTPS). The value must be the API base
44
44
  (`…/v1`), not the Console URL.
45
45
 
46
+ ## Project environments
47
+
48
+ On Appwrite Cloud, a project can have environments (for example `staging`).
49
+ To point a local server at one, keep `APPWRITE_PROJECT_ID` set to the ID of
50
+ the project it belongs to and add the environment's ID:
51
+
52
+ ```bash
53
+ APPWRITE_PROJECT_ID=<YOUR_PROJECT_ID> \
54
+ APPWRITE_ENVIRONMENT_ID=staging \
55
+ APPWRITE_API_KEY=<YOUR_API_KEY> \
56
+ uvx mcp-server-appwrite
57
+ ```
58
+
59
+ Every request then carries `X-Appwrite-Environment: staging` next to the
60
+ project's ID. The environment is fixed for the server's lifetime: a tool call
61
+ that names another `environment_id` is refused rather than run in this one.
62
+ Run one server per environment you want the agent to reach.
63
+
64
+ ## Reasons on writes
65
+
66
+ Every write the agent makes through `appwrite_call_write_tool` carries a
67
+ one-line `reason`, which Appwrite records on the project's audit log
68
+ (`X-Appwrite-Reason`). Self-hosted Appwrite versions that predate the header
69
+ ignore it.
70
+
46
71
  ## Connect your client
47
72
 
48
73
  <details open>
@@ -35,10 +35,10 @@ flowchart LR
35
35
 
36
36
  | Tool | What it does |
37
37
  | --- | --- |
38
- | `appwrite_get_context` | Workspace summary. API key → project + readable service totals/samples. OAuth → also account, organization, discovered projects. |
38
+ | `appwrite_get_context` | Workspace summary. API key → project + readable service totals/samples. OAuth → also account, organization, discovered projects. A project in focus lists its environments; `environment_id` summarizes one of them instead. |
39
39
  | `appwrite_search_tools` | Searches the internal catalog at runtime. |
40
40
  | `appwrite_call_tool` | Runs a read catalog tool (`list`, `get`). |
41
- | `appwrite_call_write_tool` | Runs every other catalog tool (create, update, delete, upsert, unknown verbs). Requires `confirm_write=true`. |
41
+ | `appwrite_call_write_tool` | Runs every other catalog tool (create, update, delete, upsert, unknown verbs). Requires `confirm_write=true` and a `reason`. |
42
42
  | `appwrite_search_docs` | Semantic search over Appwrite docs (conditional — see above). |
43
43
 
44
44
  ## Behavior
@@ -50,7 +50,21 @@ flowchart LR
50
50
  idempotent, `appwrite_call_write_tool` is destructive. Each search result
51
51
  names its `call_tool`, and calling a tool through the other one returns an
52
52
  error naming the right one.
53
- - **Writes** require `confirm_write=true`.
53
+ - **Writes** require `confirm_write=true` and a `reason`: one line, at most 256
54
+ characters, saying why the change is made. It is sent as `X-Appwrite-Reason`
55
+ (percent-encoded like `encodeURIComponent`), and Appwrite records it on the
56
+ project's audit log, shown in the Activity tab next to the change. A reason
57
+ Appwrite would refuse (blank, too long, or with line breaks or other control
58
+ characters) is refused before any request is made. Reads send no reason.
59
+ - **Environments**: `appwrite_call_tool`, `appwrite_call_write_tool`,
60
+ `appwrite_get_context` and `appwrite_show` take an optional `environment_id`
61
+ to act in one of a project's environments (Appwrite Cloud). It is sent as
62
+ `X-Appwrite-Environment` together with the project's own ID in
63
+ `X-Appwrite-Project`, since an environment's project ID is internal, and
64
+ needs `project_id` on the hosted server. `appwrite_get_context` with
65
+ `project_id` lists the project's environments (with the `project.read` scope;
66
+ self-hosted Appwrite has none). Over stdio the environment is fixed by
67
+ `APPWRITE_ENVIRONMENT_ID`, and a call naming another one is refused.
54
68
  - **Annotations**: every exposed tool sets a `title` and all four hints
55
69
  (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`)
56
70
  explicitly, as the Anthropic and OpenAI connector directories require.
@@ -35,7 +35,12 @@ from . import executions, files, protocol, resource, usage, users
35
35
 
36
36
  Run = Callable[[dict[str, Any], bool], list[Any]]
37
37
  """Runs a hidden catalog tool from raw call arguments (``tool_name``,
38
- ``arguments``, ``project_id``, ``confirm_write``), as a write or a read."""
38
+ ``arguments``, ``project_id``, ``environment_id``, ``confirm_write``,
39
+ ``reason``), as a write or a read."""
40
+
41
+ Read = Callable[[str, Any, dict[str, Any]], dict[str, Any]]
42
+ """Reads one catalog tool's JSON result in the project (and environment) a
43
+ view shows: ``(tool_name, project_id, arguments)``."""
39
44
 
40
45
  Handler = Callable[[dict[str, Any]], types.CallToolResult]
41
46
 
@@ -100,26 +105,39 @@ class Views:
100
105
 
101
106
  def _show(self, arguments: dict[str, Any]) -> types.CallToolResult:
102
107
  view = arguments.get("view")
108
+ # A view shows one project, or one of its environments, and every read
109
+ # it makes stays there.
110
+ environment_id = arguments.get("environment_id") or None
111
+
112
+ def read(tool_name: str, project_id: Any, target: dict[str, Any]) -> Any:
113
+ return self._read(tool_name, project_id, target, environment_id)
114
+
115
+ def preview(project_id: Any, target: dict[str, Any]) -> str | None:
116
+ return self._preview(project_id, target, environment_id)
117
+
103
118
  if view == View.TABLE:
104
- data = self._table(arguments)
119
+ data = self._table(read, arguments)
105
120
  text = _table_summary(data)
106
121
  elif view == View.DEPLOYMENT:
107
- data = self._deployment(arguments)
122
+ data = self._deployment(read, arguments)
108
123
  text = _deployment_summary(data)
109
124
  elif view == View.EXECUTIONS:
110
- data = executions.read(self._read, arguments)
125
+ data = executions.read(read, arguments)
111
126
  text = executions.summary(data)
112
127
  elif view == View.FILES:
113
- data = files.read(self._read, self._preview, arguments)
128
+ data = files.read(read, preview, arguments)
114
129
  text = files.summary(data)
115
130
  elif view == View.USERS:
116
- data = users.read(self._read, arguments)
131
+ data = users.read(read, arguments)
117
132
  text = users.summary(data)
118
133
  elif view == View.USAGE:
119
- data = usage.read(self._read, arguments)
134
+ data = usage.read(read, arguments)
120
135
  text = usage.summary(data)
121
136
  else:
122
137
  raise ValueError(f"view must be one of: {', '.join(View)}. Got {view!r}.")
138
+ if environment_id:
139
+ # The view sends it back on every call it makes, as it does projectId.
140
+ data["environmentId"] = environment_id
123
141
  return types.CallToolResult(
124
142
  content=[types.TextContent(type="text", text=text)],
125
143
  structured_content=data,
@@ -160,13 +178,18 @@ class Views:
160
178
  )
161
179
 
162
180
  def _read(
163
- self, tool_name: str, project_id: Any, arguments: dict[str, Any]
181
+ self,
182
+ tool_name: str,
183
+ project_id: Any,
184
+ arguments: dict[str, Any],
185
+ environment_id: str | None = None,
164
186
  ) -> dict[str, Any]:
165
187
  result = _json(
166
188
  self._run(
167
189
  {
168
190
  "tool_name": tool_name,
169
191
  "project_id": project_id,
192
+ "environment_id": environment_id,
170
193
  "arguments": arguments,
171
194
  },
172
195
  False,
@@ -176,7 +199,12 @@ class Views:
176
199
  raise ValueError(f"{tool_name} returned an unexpected response.")
177
200
  return result
178
201
 
179
- def _preview(self, project_id: Any, arguments: dict[str, Any]) -> str | None:
202
+ def _preview(
203
+ self,
204
+ project_id: Any,
205
+ arguments: dict[str, Any],
206
+ environment_id: str | None = None,
207
+ ) -> str | None:
180
208
  """A file preview as a data URI. A file Appwrite cannot transform
181
209
  shows without one rather than failing the page."""
182
210
  try:
@@ -184,6 +212,7 @@ class Views:
184
212
  {
185
213
  "tool_name": "storage_get_file_preview",
186
214
  "project_id": project_id,
215
+ "environment_id": environment_id,
187
216
  "arguments": arguments,
188
217
  },
189
218
  False,
@@ -195,21 +224,19 @@ class Views:
195
224
  return f"data:{item.mime_type};base64,{item.data}"
196
225
  return None
197
226
 
198
- def _table(self, arguments: dict[str, Any]) -> dict[str, Any]:
227
+ def _table(self, read: Read, arguments: dict[str, Any]) -> dict[str, Any]:
199
228
  project_id = arguments.get("project_id")
200
229
  database_id = _required(arguments, "database_id")
201
230
  table_id = _required(arguments, "table_id")
202
231
  cursor = arguments.get("cursor")
203
232
  target = {"database_id": database_id, "table_id": table_id}
204
233
 
205
- table = self._read("tables_db_get_table", project_id, target)
234
+ table = read("tables_db_get_table", project_id, target)
206
235
  # One row past the page tells whether another page follows.
207
236
  queries = [Query.order_desc("$createdAt"), Query.limit(PAGE_SIZE + 1)]
208
237
  if cursor:
209
238
  queries.append(Query.cursor_after(str(cursor)))
210
- page = self._read(
211
- "tables_db_list_rows", project_id, {**target, "queries": queries}
212
- )
239
+ page = read("tables_db_list_rows", project_id, {**target, "queries": queries})
213
240
  rows = [_row(each) for each in page.get("rows") or []]
214
241
  more = len(rows) > PAGE_SIZE
215
242
  rows = rows[:PAGE_SIZE]
@@ -231,7 +258,7 @@ class Views:
231
258
  "next": rows[-1].get("$id") if more else None,
232
259
  }
233
260
 
234
- def _deployment(self, arguments: dict[str, Any]) -> dict[str, Any]:
261
+ def _deployment(self, read: Read, arguments: dict[str, Any]) -> dict[str, Any]:
235
262
  project_id = arguments.get("project_id")
236
263
  function_id = arguments.get("function_id")
237
264
  site_id = arguments.get("site_id")
@@ -247,13 +274,13 @@ class Views:
247
274
 
248
275
  deployment_id = arguments.get("deployment_id")
249
276
  if deployment_id:
250
- deployment = self._read(
277
+ deployment = read(
251
278
  f"{service}_get_deployment",
252
279
  project_id,
253
280
  {**target, "deployment_id": deployment_id},
254
281
  )
255
282
  else:
256
- latest = self._read(
283
+ latest = read(
257
284
  f"{service}_list_deployments",
258
285
  project_id,
259
286
  {
@@ -321,6 +348,14 @@ def _show_tool() -> types.Tool:
321
348
  "type": "string",
322
349
  "description": "Appwrite project ID the data lives in.",
323
350
  },
351
+ "environment_id": {
352
+ "type": "string",
353
+ "description": (
354
+ "ID of the project environment (Appwrite Cloud) the data "
355
+ "lives in, with project_id set to the project it belongs "
356
+ "to. Omit for the project itself."
357
+ ),
358
+ },
324
359
  "database_id": {
325
360
  "type": "string",
326
361
  "description": "view=table: the TablesDB database ID.",
@@ -424,7 +459,9 @@ def _write_tool() -> types.Tool:
424
459
  "tool_name": {"type": "string", "enum": list(WRITES)},
425
460
  "arguments": {"type": "object"},
426
461
  "project_id": {"type": "string"},
462
+ "environment_id": {"type": "string"},
427
463
  "confirm_write": {"type": "boolean"},
464
+ "reason": {"type": "string"},
428
465
  },
429
466
  "required": ["tool_name", "arguments", "confirm_write"],
430
467
  "additionalProperties": False,
@@ -80,7 +80,8 @@
80
80
  "lines": [
81
81
  "Using these tools",
82
82
  "- Use appwrite_get_context to understand the current connection and available project resources, then use appwrite_search_tools to find the operation you need.",
83
- "- Run read operations (list, get) with appwrite_call_tool. Run every other operation (create, update, delete, upsert) with appwrite_call_write_tool and confirm_write=true; each search result names its call_tool."
83
+ "- Run read operations (list, get) with appwrite_call_tool. Run every other operation (create, update, delete, upsert) with appwrite_call_write_tool and confirm_write=true; each search result names its call_tool.",
84
+ "- Give every write a reason: one line (max 256 characters) saying why you are making the change, such as \"Add the orders table the checkout flow reads\". Appwrite records it on the project's audit log, where the user reviews it in the Activity tab next to the change."
84
85
  ]
85
86
  },
86
87
  {
@@ -92,7 +93,7 @@
92
93
  {
93
94
  "transports": ["stdio"],
94
95
  "lines": [
95
- "- This local connection uses the API key, endpoint, and project configured in the server environment. Appwrite API calls target that configured APPWRITE_PROJECT_ID by default.",
96
+ "- This local connection uses the API key, endpoint, and project configured in the server environment. Appwrite API calls target that configured APPWRITE_PROJECT_ID by default, and act in the project environment APPWRITE_ENVIRONMENT_ID names when it is set.",
96
97
  "- Project API keys cannot manage dedicated PostgreSQL/MySQL databases, domain registration, migrations or usage metrics; those need the hosted server at mcp.appwrite.io.",
97
98
  "- Large results are stored as resources; read the URI returned by the tool."
98
99
  ]
@@ -103,6 +104,7 @@
103
104
  "- You authenticate against the Appwrite console, which can list your organizations and projects but stores no project data itself.",
104
105
  "- Project-scoped tools (every database type, users, teams, storage, functions, sites, messaging, firewall, advisor) need a target project: use appwrite_get_context first, then pass the selected project id as project_id to appwrite_call_tool or appwrite_call_write_tool.",
105
106
  "- Organization-scoped console tools (e.g. creating a project or managing domains) need organization_id.",
107
+ "- A project can have environments, such as staging, each with its own resources and users. appwrite_get_context with project_id lists them. To act in one, pass its ID as environment_id together with the project's ID as project_id; without environment_id, calls act in the project itself.",
106
108
  "- File/image uploads: pass a public URL as the file argument (e.g. {\"url\": \"https://...\"}) so the server downloads it directly; for very small files you may pass inline base64 ({\"filename\": ..., \"content\": ..., \"encoding\": \"base64\"}).",
107
109
  "- Hosted HTTP returns tool results inline, including images and other binary payloads."
108
110
  ]
@@ -576,6 +576,7 @@ details.headers td { height: 32px; max-width: none; white-space: normal; overflo
576
576
  function showArgs(extra) {
577
577
  const args = { view: data.view };
578
578
  if (data.projectId) args.project_id = data.projectId;
579
+ if (data.environmentId) args.environment_id = data.environmentId;
579
580
  return { ...args, ...extra };
580
581
  }
581
582
 
@@ -728,6 +729,7 @@ details.headers td { height: 32px; max-width: none; white-space: normal; overflo
728
729
  const result = await callTool(WRITE_TOOL, {
729
730
  tool_name: "tables_db_update_row",
730
731
  project_id: data.projectId || undefined,
732
+ environment_id: data.environmentId || undefined,
731
733
  confirm_write: true,
732
734
  arguments: { database_id: data.databaseId, table_id: data.tableId, row_id: row.$id, data: values },
733
735
  });
@@ -1320,7 +1322,8 @@ details.headers td { height: 32px; max-width: none; white-space: normal; overflo
1320
1322
  render();
1321
1323
  try {
1322
1324
  const result = await callTool(WRITE_TOOL, {
1323
- tool_name: change.tool, project_id: data.projectId || undefined, confirm_write: true, arguments: change.arguments,
1325
+ tool_name: change.tool, project_id: data.projectId || undefined, environment_id: data.environmentId || undefined,
1326
+ confirm_write: true, arguments: change.arguments,
1324
1327
  });
1325
1328
  const updated = { ...user, ...(result.structuredContent || {}) };
1326
1329
  data.user = updated;
@@ -1515,6 +1518,7 @@ details.headers td { height: 32px; max-width: none; white-space: normal; overflo
1515
1518
  await callTool(WRITE_TOOL, {
1516
1519
  tool_name: data.kind === "site" ? "sites_update_deployment_status" : "functions_update_deployment_status",
1517
1520
  project_id: data.projectId || undefined,
1521
+ environment_id: data.environmentId || undefined,
1518
1522
  confirm_write: true,
1519
1523
  arguments: { [data.kind === "site" ? "site_id" : "function_id"]: data.resourceId, deployment_id: data.deployment.id },
1520
1524
  });
@@ -11,6 +11,7 @@ from appwrite_console.models.user import User
11
11
  from appwrite_console.query import Query
12
12
 
13
13
  from .constants import REDACTED_KEYS, SERVICE_PROBES
14
+ from .headers import environment, scoped
14
15
 
15
16
  ContextClientFactory = Callable[[str | None, str | None], Client]
16
17
  DEFAULT_SERVICE_PROJECT_LIMIT = 5
@@ -25,10 +26,17 @@ def get_appwrite_context(
25
26
  client_factory: ContextClientFactory | None = None,
26
27
  project_id: str | None = None,
27
28
  organization_id: str | None = None,
29
+ environment_id: str | None = None,
28
30
  include_services: bool = True,
29
31
  sample_limit: int = 5,
30
32
  service_detail: str = DEFAULT_SERVICE_DETAIL,
31
33
  ) -> dict[str, Any]:
34
+ """Summarize what the connection can reach.
35
+
36
+ A project in focus (``project_id``, or the API key's project) lists its
37
+ environments, so the model can pass one as ``environment_id``. With
38
+ ``environment_id`` (or ``APPWRITE_ENVIRONMENT_ID`` on the API-key client)
39
+ the project is summarized as that environment instead."""
32
40
  sample_limit = _normalize_sample_limit(sample_limit)
33
41
  service_detail = _normalize_service_detail(service_detail)
34
42
  client_factory = client_factory or (
@@ -50,15 +58,22 @@ def get_appwrite_context(
50
58
  project = _get_current_project(project_client, configured_project_id)
51
59
  if project is None:
52
60
  project = {"$id": configured_project_id}
53
- context["projects"] = [
54
- _project_context(
55
- project,
56
- project_client,
57
- include_services=include_services,
58
- sample_limit=sample_limit,
59
- service_detail=service_detail,
60
- )
61
- ]
61
+ summary = _project_context(
62
+ project,
63
+ project_client,
64
+ include_services=include_services,
65
+ sample_limit=sample_limit,
66
+ service_detail=service_detail,
67
+ )
68
+ acting_in = environment(project_client)
69
+ if acting_in:
70
+ summary["environmentId"] = acting_in
71
+ else:
72
+ # Listed only from the project itself: an environment has none.
73
+ environments = _list_environments(project_client)
74
+ if environments is not None:
75
+ summary["environments"] = environments
76
+ context["projects"] = [summary]
62
77
  context["serviceSummary"] = _service_summary_metadata(
63
78
  include_services=include_services,
64
79
  effective_include_services=include_services,
@@ -137,18 +152,36 @@ def get_appwrite_context(
137
152
  if needs_project_client
138
153
  else console_client
139
154
  )
155
+ environments = None
156
+ requested = project_id is not None and discovered_project_id == project_id
157
+ if project_id and requested:
158
+ # Environments are listed through the project they belong to, and
159
+ # only for the project asked about: a request per discovered
160
+ # project would multiply with the account's projects.
161
+ parent_client = (
162
+ project_client
163
+ if needs_project_client
164
+ else client_factory(project_id, project_organization_id)
165
+ )
166
+ environments = _list_environments(parent_client)
167
+ if environment_id:
168
+ project_client = scoped(parent_client, environment_id=environment_id)
169
+ project = _get_current_project(project_client, project_id) or project
140
170
  if project_id and project == {"$id": project_id}:
141
171
  project = _get_current_project(project_client, project_id) or project
142
- projects.append(
143
- _project_context(
144
- project,
145
- project_client,
146
- organization_id=project_organization_id,
147
- include_services=effective_include_services,
148
- sample_limit=sample_limit,
149
- service_detail=service_detail,
150
- )
172
+ summary = _project_context(
173
+ project,
174
+ project_client,
175
+ organization_id=project_organization_id,
176
+ include_services=effective_include_services,
177
+ sample_limit=sample_limit,
178
+ service_detail=service_detail,
151
179
  )
180
+ if requested and environment_id:
181
+ summary["environmentId"] = environment_id
182
+ if environments is not None:
183
+ summary["environments"] = environments
184
+ projects.append(summary)
152
185
 
153
186
  context["projects"] = projects
154
187
  context["serviceSummary"] = _service_summary_metadata(
@@ -202,6 +235,28 @@ def _get_current_project(client: Client, project_id: str) -> dict[str, Any] | No
202
235
  return {"$id": project_id, "error": result.error} if not result.ok else None
203
236
 
204
237
 
238
+ def _list_environments(client: Client) -> list[dict[str, Any]] | None:
239
+ """The environments of ``client``'s project, by the ID passed as
240
+ ``environment_id``. ``None`` when they cannot be listed: environments are
241
+ an Appwrite Cloud feature (self-hosted Appwrite has no such route), and
242
+ listing them needs the ``project.read`` scope."""
243
+ result = _safe_call(client, "get", "/project/environments")
244
+ if not result.ok or not isinstance(result.value, dict):
245
+ return None
246
+ items = result.value.get("environments")
247
+ if not isinstance(items, list):
248
+ return None
249
+ return [
250
+ {
251
+ key: item[key]
252
+ for key in ("$id", "name")
253
+ if isinstance(item.get(key), str) and item[key]
254
+ }
255
+ for item in items
256
+ if isinstance(item, dict) and isinstance(item.get("$id"), str)
257
+ ]
258
+
259
+
205
260
  def _accessible_resource_ids(client: Client, resource: str) -> list[dict[str, Any]]:
206
261
  """Fallback discovery via the OAuth2 accessible-resources endpoints
207
262
  (``/oauth2/<project>/organizations`` / ``/oauth2/<project>/projects``).
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
- name: mcp_server_appwrite-2.2.3/src/mcp_server_appwrite/data/docs_index.npz
2
+ name: mcp_server_appwrite-2.3.0/src/mcp_server_appwrite/data/docs_index.npz
3
3
  size: 35737872 bytes
4
4
  sha256: 53ab05a0732fc87657755ce579c09867da3b176244ce599ad1fdaecdcd112ac7
@@ -40,6 +40,13 @@ class WriteConfirmationRequired(RuntimeError):
40
40
  """A mutating hidden tool was called without explicit confirmation."""
41
41
 
42
42
 
43
+ class WriteReasonRequired(WriteConfirmationRequired):
44
+ """A mutating hidden tool was called without a usable ``reason``.
45
+
46
+ Like a missing ``confirm_write``, this is the model leaving out something a
47
+ write must carry, so it is classified and monitored the same way."""
48
+
49
+
43
50
  class WrongCallTool(ValueError):
44
51
  """A hidden tool was called through the public tool for the other
45
52
  classification: a mutation through the read-only call tool, or a read
@@ -133,6 +133,7 @@ def capture_appwrite_exception(
133
133
  classification: str,
134
134
  project_id: str | None = None,
135
135
  organization_id: str | None = None,
136
+ environment_id: str | None = None,
136
137
  ) -> bool:
137
138
  """Capture publishable Appwrite API failures.
138
139
 
@@ -147,6 +148,7 @@ def capture_appwrite_exception(
147
148
  "appwrite.error_type": getattr(exc, "type", None),
148
149
  "appwrite.project_id": project_id,
149
150
  "appwrite.organization_id": organization_id,
151
+ "appwrite.environment_id": environment_id,
150
152
  }
151
153
  return capture_exception(
152
154
  exc,
@@ -160,6 +162,7 @@ def capture_appwrite_exception(
160
162
  "error_type": getattr(exc, "type", None),
161
163
  "project_id": project_id,
162
164
  "organization_id": organization_id,
165
+ "environment_id": environment_id,
163
166
  },
164
167
  },
165
168
  transaction=f"appwrite.{service or 'unknown'}.{action or 'unknown'}",