mcp-server-appwrite 2.1.3__tar.gz → 2.2.1__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 (139) hide show
  1. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/PKG-INFO +1 -1
  2. mcp_server_appwrite-2.2.1/docs/apps.md +101 -0
  3. mcp_server_appwrite-2.2.1/src/mcp_server_appwrite/apps/executions.py +188 -0
  4. mcp_server_appwrite-2.2.1/src/mcp_server_appwrite/apps/files.py +210 -0
  5. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/apps/protocol.py +32 -0
  6. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/apps/resource.py +2 -1
  7. mcp_server_appwrite-2.2.1/src/mcp_server_appwrite/apps/usage.py +188 -0
  8. mcp_server_appwrite-2.2.1/src/mcp_server_appwrite/apps/users.py +168 -0
  9. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/apps/views.py +132 -11
  10. mcp_server_appwrite-2.2.1/src/mcp_server_appwrite/assets/view.html +1546 -0
  11. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/data/docs_index.npz +1 -1
  12. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/server.py +1 -0
  13. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/telemetry.py +38 -0
  14. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/e2e/support.py +329 -8
  15. mcp_server_appwrite-2.2.1/tests/e2e/test_apps.py +843 -0
  16. mcp_server_appwrite-2.1.3/docs/apps.md +0 -63
  17. mcp_server_appwrite-2.1.3/src/mcp_server_appwrite/assets/view.html +0 -810
  18. mcp_server_appwrite-2.1.3/tests/e2e/test_apps.py +0 -380
  19. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/.dockerignore +0 -0
  20. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/.env.example +0 -0
  21. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/.github/workflows/ci.yml +0 -0
  22. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/.github/workflows/production.yml +0 -0
  23. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/.github/workflows/publish.yml +0 -0
  24. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/.github/workflows/staging.yml +0 -0
  25. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/.github/workflows/update-docs-index.yml +0 -0
  26. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/.gitignore +0 -0
  27. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/.python-version +0 -0
  28. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/AGENTS.md +0 -0
  29. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/CLAUDE.md +0 -0
  30. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/Dockerfile +0 -0
  31. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/LICENSE +0 -0
  32. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/README.md +0 -0
  33. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/compose.yaml +0 -0
  34. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/docs/appwrite-mcp-flow.png +0 -0
  35. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/docs/appwrite-mcp-flow.svg +0 -0
  36. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/docs/authentication.md +0 -0
  37. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/docs/development.md +0 -0
  38. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/docs/documentation-search.md +0 -0
  39. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/docs/events.md +0 -0
  40. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/docs/flags.md +0 -0
  41. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/docs/images/claude-desktop-local-mcp-servers.png +0 -0
  42. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/docs/images/codex-mcp.png +0 -0
  43. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/docs/images/cursor-mcp.png +0 -0
  44. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/docs/self-hosted.md +0 -0
  45. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/docs/tool-surface.md +0 -0
  46. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/glama.json +0 -0
  47. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/pyproject.toml +0 -0
  48. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/scripts/build_docs_index.py +0 -0
  49. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/scripts/docs_release_notes.py +0 -0
  50. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/scripts/render_server_json.py +0 -0
  51. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/server.template.json +0 -0
  52. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/__init__.py +0 -0
  53. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/__main__.py +0 -0
  54. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/apps/__init__.py +0 -0
  55. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/assets/__init__.py +0 -0
  56. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/assets/favicon.svg +0 -0
  57. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/assets/instructions.json +0 -0
  58. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/auth.py +0 -0
  59. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/catalog_policy.py +0 -0
  60. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/constants.py +0 -0
  61. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/context.py +0 -0
  62. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/docs.py +0 -0
  63. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/docs_index.py +0 -0
  64. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/docs_search.py +0 -0
  65. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/docs_source.py +0 -0
  66. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/error_classification.py +0 -0
  67. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/error_monitoring.py +0 -0
  68. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/events/__init__.py +0 -0
  69. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/events/bus.py +0 -0
  70. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/events/catalog.py +0 -0
  71. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/events/delivery.py +0 -0
  72. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/events/egress.py +0 -0
  73. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/events/envelope.py +0 -0
  74. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/events/errors.py +0 -0
  75. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/events/ingress.py +0 -0
  76. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/events/projection.py +0 -0
  77. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/events/protocol.py +0 -0
  78. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/events/streams.py +0 -0
  79. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/events/subscriptions.py +0 -0
  80. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/events/webhooks.py +0 -0
  81. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/flags.py +0 -0
  82. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/http_app.py +0 -0
  83. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/instructions.py +0 -0
  84. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/legacy.py +0 -0
  85. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/operator.py +0 -0
  86. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/service.py +0 -0
  87. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/tool_manager.py +0 -0
  88. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/e2e/fixtures/appwrite/deployment_function_duplicate.json +0 -0
  89. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/e2e/fixtures/appwrite/deployment_function_failed.json +0 -0
  90. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/e2e/fixtures/appwrite/deployment_function_ready.json +0 -0
  91. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/e2e/fixtures/appwrite/deployment_site_ready.json +0 -0
  92. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/e2e/fixtures/appwrite/execution_async_failed.json +0 -0
  93. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/e2e/fixtures/appwrite/execution_async_waiting.json +0 -0
  94. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/e2e/fixtures/appwrite/execution_sync_failed.json +0 -0
  95. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/e2e/fixtures/appwrite/file_created.json +0 -0
  96. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/e2e/fixtures/appwrite/function_activation.json +0 -0
  97. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/e2e/fixtures/appwrite/row_created.json +0 -0
  98. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/e2e/fixtures/appwrite/user_created.json +0 -0
  99. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/e2e/test_events_ingress.py +0 -0
  100. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/e2e/test_events_protocol.py +0 -0
  101. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/e2e/test_events_stream.py +0 -0
  102. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/e2e/test_events_subscribe.py +0 -0
  103. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/e2e/test_stdio.py +0 -0
  104. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/e2e/test_tools.py +0 -0
  105. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/integration/support.py +0 -0
  106. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/integration/test_avatars.py +0 -0
  107. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/integration/test_console_sdk_oauth.py +0 -0
  108. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/integration/test_docs_index.py +0 -0
  109. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/integration/test_docs_source.py +0 -0
  110. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/integration/test_functions.py +0 -0
  111. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/integration/test_locale.py +0 -0
  112. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/integration/test_messaging.py +0 -0
  113. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/integration/test_oauth_discovery.py +0 -0
  114. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/integration/test_operator.py +0 -0
  115. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/integration/test_sites.py +0 -0
  116. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/integration/test_storage.py +0 -0
  117. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/integration/test_tables_db.py +0 -0
  118. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/integration/test_teams.py +0 -0
  119. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/integration/test_users.py +0 -0
  120. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/test_all.py +0 -0
  121. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/unit/fixtures/jwe-v1.json +0 -0
  122. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/unit/test_auth.py +0 -0
  123. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/unit/test_context.py +0 -0
  124. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/unit/test_docs.py +0 -0
  125. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/unit/test_docs_search.py +0 -0
  126. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/unit/test_error_classification.py +0 -0
  127. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/unit/test_error_monitoring.py +0 -0
  128. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/unit/test_events_delivery.py +0 -0
  129. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/unit/test_events_egress.py +0 -0
  130. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/unit/test_events_envelope.py +0 -0
  131. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/unit/test_events_protocol.py +0 -0
  132. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/unit/test_http_app.py +0 -0
  133. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/unit/test_instructions.py +0 -0
  134. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/unit/test_operator.py +0 -0
  135. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/unit/test_release_metadata.py +0 -0
  136. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/unit/test_server.py +0 -0
  137. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/unit/test_service.py +0 -0
  138. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/unit/test_telemetry.py +0 -0
  139. {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: mcp-server-appwrite
3
- Version: 2.1.3
3
+ Version: 2.2.1
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
@@ -0,0 +1,101 @@
1
+ # MCP Apps
2
+
3
+ [MCP Apps](https://modelcontextprotocol.io/extensions/apps/overview) (SEP-1865,
4
+ extension `io.modelcontextprotocol/ui`) let a tool result render as an
5
+ interactive view inside the chat. This server offers one view,
6
+ `ui://appwrite/view`, behind the `apps` flag (`MCP_APPS=1`, both transports).
7
+
8
+ ## Surface
9
+
10
+ | Name | Seen by | What it does |
11
+ | --- | --- | --- |
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`. |
14
+ | `ui://appwrite/view` | host | One HTML document (`assets/view.html`) that renders every view. |
15
+
16
+ `appwrite_show` returns a text summary in `content`, which is what the model and
17
+ hosts without MCP Apps see, and the data in `structuredContent` for the view.
18
+ The view calls `appwrite_show` again to page, refresh and follow a build (every
19
+ 3 seconds until it is ready, failed or canceled), and to follow executions
20
+ while any on the page, or the opened one, is waiting or processing. Appwrite
21
+ keeps an execution's response body only for the synchronous call that ran it,
22
+ so the view shows it only when present. An edited row or a cancelled
23
+ build goes through a confirmation dialog in the view, and only then through
24
+ `appwrite_view_write`, as do blocking or unblocking a user, marking their
25
+ email verified or not, and changing their labels. A successful change and a
26
+ finished build are passed to the model with `ui/update-model-context` when the
27
+ host supports it.
28
+
29
+ The files view has no network access, so it cannot load a file's preview
30
+ URL. It calls `appwrite_show` again with `thumbnails: true`, and the server
31
+ renders the page's images through Appwrite's image transformations
32
+ (`storage_get_file_preview`, eight at a time) and returns them as data URIs.
33
+ The model's own call leaves them out, since some hosts put
34
+ `structuredContent` in the model's context.
35
+
36
+ The usage view reads the Usage API (`usage_list_events` and
37
+ `usage_list_gauges`). Appwrite keeps usage history for as long as the plan
38
+ allows; a range past it returns Appwrite's own error, which the view shows in
39
+ place of the chart it was switching to.
40
+
41
+ ## Who is offered the view
42
+
43
+ Hosts that declare the extension in their client capabilities get the two tools
44
+ and the resource. Hosts that declare capabilities without it do not. The
45
+ hosted transport is stateless, so a 2025-era client's `tools/list` carries no
46
+ capabilities; it is offered the view, and a host that cannot render it uses the
47
+ text summary. 2026-07-28 clients send `clientCapabilities` with every request,
48
+ and stdio has the handshake, so both are gated exactly. `tools/call` is not
49
+ gated, because the view's own calls reach the server through the host.
50
+
51
+ ## Telemetry
52
+
53
+ On the hosted transport, two counters show which views people use:
54
+
55
+ - `mcp.apps.views` counts `appwrite_show` calls by `view` (`invalid` for a name
56
+ that is not one), `status` (`success` or `error`), `client_id`, and `origin`:
57
+ `model` when the model opens a view, `view` when the view refreshes, pages,
58
+ polls a build or opens a row on its own. Views opened per view type is
59
+ `origin="model"`; how much people then use each view is `origin="view"`.
60
+ - `mcp.apps.writes` counts `appwrite_view_write` calls by `action` (the
61
+ allowlisted write, or `invalid`), `status` and `client_id`.
62
+
63
+ The view marks its own calls with `"io.appwrite/origin": "view"` in the
64
+ request's `_meta`. Hosts forward a view's `tools/call` params as sent; a host
65
+ that drops `_meta` makes those calls count as `origin="model"`. Both counters
66
+ feed the MCP Apps row of the MCP dashboard (appwrite-labs/monitoring
67
+ `dashboards/Cloud/mcp.json`).
68
+
69
+ ## Security
70
+
71
+ - The resource declares no CSP domains, so hosts apply the spec's restrictive
72
+ default: no network access, inline script and style only. The view never
73
+ holds a token; every Appwrite call is a `tools/call` the host proxies to this
74
+ server with the user's own credentials.
75
+ - Everything the view renders comes from Appwrite. It builds the DOM with
76
+ `textContent` and `createElement` only (the e2e flow fails on `innerHTML`),
77
+ because a script injected through that data could call `appwrite_view_write`.
78
+ The allowlist bounds what such a script could do, and `confirm_write` is
79
+ required as for `appwrite_call_write_tool`.
80
+ - User documents carry the password hash, prefs and messaging targets, and
81
+ sessions carry OAuth provider tokens. The users view, and the user writes'
82
+ results, carry only the fields the view shows.
83
+
84
+ ## Trying it in a real host
85
+
86
+ The ext-apps repository ships `basic-host`, the reference host. It connects
87
+ without auth, so point it at a local server whose requests carry a token, for
88
+ example the e2e harness (`tests/e2e/support.py`) with its fake Appwrite:
89
+
90
+ ```bash
91
+ git clone --depth 1 https://github.com/modelcontextprotocol/ext-apps.git
92
+ cd ext-apps/examples/basic-host && npm install
93
+ NODE_ENV=development npx cross-env INPUT=index.html vite build
94
+ NODE_ENV=development npx cross-env INPUT=sandbox.html vite build
95
+ SERVERS='["http://localhost:3001/mcp"]' bun serve.ts # host on :8080
96
+ ```
97
+
98
+ Pick `appwrite_show`, call it with
99
+ `{"view":"table","project_id":"…","database_id":"…","table_id":"…"}`, and the
100
+ view renders under the tool call. The host's theme toggle switches the view
101
+ between light and dark.
@@ -0,0 +1,188 @@
1
+ """``view=executions``: a Function's executions, and one execution in full.
2
+
3
+ The list is a page of executions, newest first, optionally narrowed to one
4
+ status. With ``execution_id`` the payload also carries that execution's
5
+ request, response headers, logs and errors (Appwrite keeps a response body
6
+ only for the synchronous call that ran it), so "why did the last run fail?" opens on
7
+ the answer. The view polls while an execution on the page or the opened one
8
+ is still waiting or running.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from collections.abc import Callable
14
+ from typing import Any
15
+
16
+ from appwrite_console.query import Query
17
+
18
+ from ..constants import SHOW_TOOL_NAME
19
+
20
+ Read = Callable[[str, Any, dict[str, Any]], dict[str, Any]]
21
+ """Runs a hidden read tool: ``(tool name, project ID, arguments)``."""
22
+
23
+ PAGE_SIZE = 25
24
+
25
+ STATUSES = ("waiting", "processing", "completed", "failed", "scheduled")
26
+ RUNNING = frozenset({"waiting", "processing"})
27
+
28
+ LOG_LINES = 200
29
+ """Lines of logs and of errors an execution carries."""
30
+
31
+ BODY_LIMIT = 8192
32
+ """Characters of the response body an execution carries."""
33
+
34
+ SUMMARY_ROWS = 10
35
+ SUMMARY_LOG_LINES = 20
36
+
37
+
38
+ def read(read: Read, arguments: dict[str, Any]) -> dict[str, Any]:
39
+ project_id = arguments.get("project_id")
40
+ function_id = arguments.get("function_id")
41
+ if not isinstance(function_id, str) or not function_id:
42
+ raise ValueError("function_id is required for view=executions.")
43
+ status = arguments.get("status") or None
44
+ if status is not None and status not in STATUSES:
45
+ raise ValueError(f"status must be one of: {', '.join(STATUSES)}.")
46
+ cursor = arguments.get("cursor") or None
47
+ execution_id = arguments.get("execution_id") or None
48
+ target = {"function_id": function_id}
49
+
50
+ function = read("functions_get", project_id, target)
51
+ # One past the page tells whether another page follows.
52
+ queries = [Query.order_desc("$createdAt"), Query.limit(PAGE_SIZE + 1)]
53
+ if status:
54
+ queries.append(Query.equal("status", status))
55
+ if cursor:
56
+ queries.append(Query.cursor_after(str(cursor)))
57
+ page = read("functions_list_executions", project_id, {**target, "queries": queries})
58
+ executions = page.get("executions") or []
59
+ more = len(executions) > PAGE_SIZE
60
+ executions = executions[:PAGE_SIZE]
61
+
62
+ execution = None
63
+ if execution_id:
64
+ execution = _detail(
65
+ read(
66
+ "functions_get_execution",
67
+ project_id,
68
+ {**target, "execution_id": execution_id},
69
+ )
70
+ )
71
+ rows = [_summary(each) for each in executions]
72
+ return {
73
+ "view": "executions",
74
+ "projectId": project_id,
75
+ "functionId": function_id,
76
+ "name": function.get("name") or function_id,
77
+ "runtime": function.get("runtime"),
78
+ "status": status,
79
+ "executions": rows,
80
+ "total": int(page.get("total", len(rows))),
81
+ "cursor": cursor,
82
+ "next": rows[-1]["id"] if more else None,
83
+ "execution": execution,
84
+ "running": any(each["status"] in RUNNING for each in rows)
85
+ or bool(execution and execution["status"] in RUNNING),
86
+ }
87
+
88
+
89
+ def _summary(execution: dict[str, Any]) -> dict[str, Any]:
90
+ return {
91
+ "id": execution.get("$id"),
92
+ "status": execution.get("status") or "unknown",
93
+ "trigger": execution.get("trigger"),
94
+ "method": execution.get("requestMethod") or None,
95
+ "path": execution.get("requestPath") or None,
96
+ "statusCode": int(execution.get("responseStatusCode") or 0),
97
+ "duration": execution.get("duration"),
98
+ "createdAt": execution.get("$createdAt"),
99
+ "scheduledAt": execution.get("scheduledAt") or None,
100
+ }
101
+
102
+
103
+ def _detail(execution: dict[str, Any]) -> dict[str, Any]:
104
+ body = execution.get("responseBody") or ""
105
+ logs = (execution.get("logs") or "").splitlines()
106
+ errors = (execution.get("errors") or "").splitlines()
107
+ return {
108
+ **_summary(execution),
109
+ "deploymentId": execution.get("deploymentId") or None,
110
+ "requestHeaders": _headers(execution.get("requestHeaders")),
111
+ "responseHeaders": _headers(execution.get("responseHeaders")),
112
+ "responseBody": body[:BODY_LIMIT],
113
+ "bodyTruncated": len(body) > BODY_LIMIT,
114
+ "logs": logs[-LOG_LINES:],
115
+ "logsTruncated": len(logs) > LOG_LINES,
116
+ "errors": errors[-LOG_LINES:],
117
+ "errorsTruncated": len(errors) > LOG_LINES,
118
+ }
119
+
120
+
121
+ def _headers(headers: Any) -> list[dict[str, str]]:
122
+ return [
123
+ {"name": str(each.get("name", "")), "value": str(each.get("value", ""))}
124
+ for each in headers or []
125
+ if isinstance(each, dict)
126
+ ]
127
+
128
+
129
+ def _request(execution: dict[str, Any]) -> str:
130
+ if not execution["method"]:
131
+ return execution["trigger"] or ""
132
+ return f"{execution['method']} {execution['path'] or '/'}"
133
+
134
+
135
+ def summary(data: dict[str, Any]) -> str:
136
+ rows: list[dict[str, Any]] = data["executions"]
137
+ scope = f" {data['status']}" if data["status"] else ""
138
+ lines = [
139
+ f"Function {data['name']} ({data['functionId']}): {data['total']}"
140
+ f"{scope} executions, showing {len(rows)} newest"
141
+ + (" after the cursor." if data["cursor"] else ".")
142
+ ]
143
+ if rows:
144
+ lines.append("id | status | trigger | request | code | duration | created")
145
+ for each in rows[:SUMMARY_ROWS]:
146
+ lines.append(
147
+ " | ".join(
148
+ [
149
+ each["id"],
150
+ each["status"],
151
+ each["trigger"] or "",
152
+ _request(each),
153
+ str(each["statusCode"] or ""),
154
+ _duration(each["duration"]),
155
+ each["createdAt"] or "",
156
+ ]
157
+ )
158
+ )
159
+ if len(rows) > SUMMARY_ROWS:
160
+ lines.append(f"… {len(rows) - SUMMARY_ROWS} more shown in the view.")
161
+ if data["next"]:
162
+ lines.append(
163
+ f"Next page: {SHOW_TOOL_NAME} with view=executions and "
164
+ f"cursor={data['next']}."
165
+ )
166
+ execution = data["execution"]
167
+ if execution:
168
+ lines.append(
169
+ f"Execution {execution['id']}: {execution['status']}, "
170
+ f"{_request(execution)} → {execution['statusCode'] or 'no response'} "
171
+ f"in {_duration(execution['duration'])}."
172
+ )
173
+ for label, key in (("Errors", "errors"), ("Logs", "logs")):
174
+ if execution[key]:
175
+ lines.append(f"{label} (last lines):")
176
+ lines.extend(execution[key][-SUMMARY_LOG_LINES:])
177
+ if execution["responseBody"]:
178
+ lines.append("Response body: " + execution["responseBody"][:500])
179
+ return "\n".join(lines)
180
+
181
+
182
+ def _duration(seconds: Any) -> str:
183
+ # Executions that have not run yet report 0.
184
+ if not isinstance(seconds, (int, float)) or seconds <= 0:
185
+ return "–"
186
+ if seconds < 1:
187
+ return f"{seconds * 1000:.0f}ms"
188
+ return f"{seconds:.2f}s"
@@ -0,0 +1,210 @@
1
+ """``view=files``: a Storage bucket's files, with image previews.
2
+
3
+ The list is a page of a bucket's files, newest first, optionally narrowed by a
4
+ search over file names. With ``file_id`` the payload also carries that file in
5
+ full.
6
+
7
+ The view has no network access, so it cannot load a file's preview URL. With
8
+ ``thumbnails`` the payload carries small previews of the page's images (and a
9
+ larger one of the opened file) as data URIs, rendered by Appwrite's image
10
+ transformations through the same hidden catalog read the model could make.
11
+ The model's own call leaves them out, since some hosts put
12
+ ``structuredContent`` in the model's context; the view asks for them itself.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from collections.abc import Callable
18
+ from concurrent.futures import ThreadPoolExecutor
19
+ from contextvars import copy_context
20
+ from typing import Any
21
+
22
+ from appwrite_console.query import Query
23
+
24
+ from ..constants import SHOW_TOOL_NAME
25
+
26
+ Read = Callable[[str, Any, dict[str, Any]], dict[str, Any]]
27
+ """Runs a hidden read tool: ``(tool name, project ID, arguments)``."""
28
+
29
+ Preview = Callable[[Any, dict[str, Any]], str | None]
30
+ """Renders ``storage_get_file_preview`` for ``(project ID, arguments)`` as a
31
+ data URI, ``None`` when Appwrite cannot."""
32
+
33
+ PAGE_SIZE = 24
34
+ """Files per page: fills the grid's rows of two, three, four and six."""
35
+
36
+ THUMBNAIL = 240
37
+ """Thumbnail edge in pixels: twice the grid's cell, for dense screens."""
38
+
39
+ PREVIEW = 960
40
+ """Width of the opened file's preview in pixels."""
41
+
42
+ PREVIEWABLE = frozenset(
43
+ {
44
+ "image/jpeg",
45
+ "image/png",
46
+ "image/gif",
47
+ "image/webp",
48
+ "image/avif",
49
+ "image/heic",
50
+ "image/bmp",
51
+ }
52
+ )
53
+ """Types Appwrite's image transformations accept."""
54
+
55
+ PARALLEL = 8
56
+ """Previews rendered at once. Each is one Appwrite request of a few hundred
57
+ milliseconds, so a page of 24 takes about three rounds."""
58
+
59
+ SUMMARY_ROWS = 10
60
+
61
+
62
+ def read(read: Read, preview: Preview, arguments: dict[str, Any]) -> dict[str, Any]:
63
+ project_id = arguments.get("project_id")
64
+ bucket_id = arguments.get("bucket_id")
65
+ if not isinstance(bucket_id, str) or not bucket_id:
66
+ raise ValueError("bucket_id is required for view=files.")
67
+ search = arguments.get("search") or None
68
+ cursor = arguments.get("cursor") or None
69
+ file_id = arguments.get("file_id") or None
70
+ thumbnails = arguments.get("thumbnails") is True
71
+ target = {"bucket_id": bucket_id}
72
+
73
+ bucket = read("storage_get_bucket", project_id, target)
74
+ # One past the page tells whether another page follows.
75
+ queries = [Query.order_desc("$createdAt"), Query.limit(PAGE_SIZE + 1)]
76
+ if cursor:
77
+ queries.append(Query.cursor_after(str(cursor)))
78
+ listing: dict[str, Any] = {**target, "queries": queries}
79
+ if search:
80
+ listing["search"] = str(search)
81
+ page = read("storage_list_files", project_id, listing)
82
+ rows = [_file(each) for each in page.get("files") or []]
83
+ more = len(rows) > PAGE_SIZE
84
+ rows = rows[:PAGE_SIZE]
85
+ transforms = bool(bucket.get("transformations", True))
86
+
87
+ opened = None
88
+ if file_id:
89
+ opened = _file(
90
+ read("storage_get_file", project_id, {**target, "file_id": file_id})
91
+ )
92
+
93
+ if thumbnails and transforms:
94
+ # (file, key, preview arguments) for every image preview to render.
95
+ wanted = [
96
+ (
97
+ row,
98
+ "thumbnail",
99
+ {"width": THUMBNAIL, "height": THUMBNAIL, "quality": 70},
100
+ )
101
+ for row in rows
102
+ if row["mimeType"] in PREVIEWABLE
103
+ ]
104
+ if opened and opened["mimeType"] in PREVIEWABLE:
105
+ wanted.append((opened, "preview", {"width": PREVIEW, "quality": 80}))
106
+ with ThreadPoolExecutor(max_workers=PARALLEL) as pool:
107
+ # Each render runs in a copy of this request's context, which holds
108
+ # the caller's credentials.
109
+ futures = [
110
+ pool.submit(
111
+ copy_context().run,
112
+ preview,
113
+ project_id,
114
+ {**target, "file_id": each["id"], "output": "webp", **options},
115
+ )
116
+ for each, _, options in wanted
117
+ ]
118
+ for (each, key, _), future in zip(wanted, futures):
119
+ each[key] = future.result()
120
+ return {
121
+ "view": "files",
122
+ "projectId": project_id,
123
+ "bucketId": bucket_id,
124
+ "bucket": {
125
+ "name": bucket.get("name") or bucket_id,
126
+ "maximumFileSize": bucket.get("maximumFileSize"),
127
+ "allowedFileExtensions": bucket.get("allowedFileExtensions") or [],
128
+ "encryption": bool(bucket.get("encryption")),
129
+ "antivirus": bool(bucket.get("antivirus")),
130
+ "compression": bucket.get("compression") or "none",
131
+ "fileSecurity": bool(bucket.get("fileSecurity")),
132
+ "transformations": transforms,
133
+ },
134
+ "search": search,
135
+ "files": rows,
136
+ "total": int(page.get("total", len(rows))),
137
+ "cursor": cursor,
138
+ "next": rows[-1]["id"] if more else None,
139
+ "file": opened,
140
+ "thumbnails": thumbnails,
141
+ }
142
+
143
+
144
+ def _file(document: dict[str, Any]) -> dict[str, Any]:
145
+ return {
146
+ "id": document.get("$id"),
147
+ "name": document.get("name") or "",
148
+ "mimeType": document.get("mimeType") or "",
149
+ "size": int(document.get("sizeOriginal") or 0),
150
+ "createdAt": document.get("$createdAt"),
151
+ "updatedAt": document.get("$updatedAt"),
152
+ "folder": document.get("folder") or "",
153
+ "encryption": bool(document.get("encryption")),
154
+ "compression": document.get("compression") or "none",
155
+ "chunksTotal": int(document.get("chunksTotal") or 0),
156
+ "chunksUploaded": int(document.get("chunksUploaded") or 0),
157
+ "permissions": [str(each) for each in document.get("$permissions") or []],
158
+ "thumbnail": None,
159
+ "preview": None,
160
+ }
161
+
162
+
163
+ def size(value: int) -> str:
164
+ if value < 1024:
165
+ return f"{value} B"
166
+ amount = value / 1024
167
+ for unit in ("KB", "MB"):
168
+ if amount < 1024:
169
+ return f"{amount:.1f} {unit}"
170
+ amount /= 1024
171
+ return f"{amount:.1f} GB"
172
+
173
+
174
+ def summary(data: dict[str, Any]) -> str:
175
+ rows: list[dict[str, Any]] = data["files"]
176
+ scope = f" matching {data['search']!r}" if data["search"] else ""
177
+ lines = [
178
+ f"Bucket {data['bucket']['name']} ({data['bucketId']}): {data['total']} "
179
+ f"files{scope}, showing {len(rows)} newest"
180
+ + (" after the cursor." if data["cursor"] else ".")
181
+ ]
182
+ if rows:
183
+ lines.append("id | name | type | size | uploaded")
184
+ for row in rows[:SUMMARY_ROWS]:
185
+ lines.append(
186
+ " | ".join(
187
+ [
188
+ row["id"],
189
+ row["name"],
190
+ row["mimeType"],
191
+ size(row["size"]),
192
+ row["createdAt"] or "",
193
+ ]
194
+ )
195
+ )
196
+ if len(rows) > SUMMARY_ROWS:
197
+ lines.append(f"… {len(rows) - SUMMARY_ROWS} more shown in the view.")
198
+ if data["next"]:
199
+ lines.append(
200
+ f"Next page: {SHOW_TOOL_NAME} with view=files and cursor={data['next']}."
201
+ )
202
+ opened = data["file"]
203
+ if opened:
204
+ lines.append(
205
+ f"File {opened['id']} ({opened['name']}): {opened['mimeType']}, "
206
+ f"{size(opened['size'])}, uploaded {opened['createdAt']}"
207
+ + (", encrypted" if opened["encryption"] else "")
208
+ + "."
209
+ )
210
+ return "\n".join(lines)
@@ -19,6 +19,8 @@ Apps are offered behind the ``apps`` flag (``MCP_APPS``); see :func:`enabled`.
19
19
  from __future__ import annotations
20
20
 
21
21
  from collections.abc import Mapping
22
+ from contextvars import ContextVar
23
+ from enum import StrEnum
22
24
  from typing import Any
23
25
 
24
26
  from mcp.server import Server, ServerRequestContext
@@ -34,6 +36,22 @@ MIME_TYPE = "text/html;profile=mcp-app"
34
36
 
35
37
  SETTINGS: dict[str, Any] = {"mimeTypes": [MIME_TYPE]}
36
38
 
39
+ ORIGIN_META_KEY = "io.appwrite/origin"
40
+ """The request ``_meta`` key the view sets to ``"view"`` on its own calls, so
41
+ telemetry can tell a view refreshing and paging itself from the model opening
42
+ one. Hosts forward a view's ``tools/call`` params as sent; a host that drops
43
+ ``_meta`` makes those calls count as the model's."""
44
+
45
+
46
+ class Origin(StrEnum):
47
+ MODEL = "model"
48
+ VIEW = "view"
49
+
50
+
51
+ _origin: ContextVar[Origin] = ContextVar(
52
+ "appwrite_mcp_apps_origin", default=Origin.MODEL
53
+ )
54
+
37
55
 
38
56
  def enabled() -> bool:
39
57
  """Whether this server offers MCP Apps."""
@@ -56,6 +74,20 @@ def supported(context: ServerRequestContext[Any, Any]) -> bool:
56
74
  return not isinstance(mime_types, list) or MIME_TYPE in mime_types
57
75
 
58
76
 
77
+ def bind_origin(context: ServerRequestContext[Any, Any]) -> None:
78
+ """Remember who made this ``tools/call``: the view, when its ``_meta``
79
+ says so, else the model. Contextvars reach the worker thread that runs the
80
+ tool."""
81
+ meta = context.meta
82
+ from_view = isinstance(meta, Mapping) and meta.get(ORIGIN_META_KEY) == Origin.VIEW
83
+ _origin.set(Origin.VIEW if from_view else Origin.MODEL)
84
+
85
+
86
+ def origin() -> Origin:
87
+ """Who made the ``tools/call`` being served."""
88
+ return _origin.get()
89
+
90
+
59
91
  def register(server: Server[Any]) -> None:
60
92
  """Advertise the extension in ``server``'s capabilities."""
61
93
  server.extensions[EXTENSION] = dict(SETTINGS)
@@ -54,7 +54,8 @@ def resource() -> types.Resource:
54
54
  uri=uri(),
55
55
  name="Appwrite view",
56
56
  description=(
57
- "Interactive Appwrite view: table rows and deployment build logs."
57
+ "Interactive Appwrite view: table rows, deployment build logs, "
58
+ "function executions, Storage files, Auth users and project usage."
58
59
  ),
59
60
  mime_type=MIME_TYPE,
60
61
  size=len(html().encode("utf-8")),