mcp-server-appwrite 2.2.0__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 (136) hide show
  1. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/PKG-INFO +1 -1
  2. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/docs/apps.md +18 -0
  3. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/apps/protocol.py +32 -0
  4. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/apps/views.py +26 -1
  5. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/assets/view.html +3 -1
  6. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/data/docs_index.npz +1 -1
  7. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/server.py +1 -0
  8. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/telemetry.py +38 -0
  9. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/e2e/support.py +2 -1
  10. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/e2e/test_apps.py +68 -4
  11. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/.dockerignore +0 -0
  12. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/.env.example +0 -0
  13. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/.github/workflows/ci.yml +0 -0
  14. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/.github/workflows/production.yml +0 -0
  15. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/.github/workflows/publish.yml +0 -0
  16. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/.github/workflows/staging.yml +0 -0
  17. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/.github/workflows/update-docs-index.yml +0 -0
  18. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/.gitignore +0 -0
  19. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/.python-version +0 -0
  20. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/AGENTS.md +0 -0
  21. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/CLAUDE.md +0 -0
  22. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/Dockerfile +0 -0
  23. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/LICENSE +0 -0
  24. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/README.md +0 -0
  25. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/compose.yaml +0 -0
  26. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/docs/appwrite-mcp-flow.png +0 -0
  27. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/docs/appwrite-mcp-flow.svg +0 -0
  28. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/docs/authentication.md +0 -0
  29. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/docs/development.md +0 -0
  30. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/docs/documentation-search.md +0 -0
  31. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/docs/events.md +0 -0
  32. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/docs/flags.md +0 -0
  33. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/docs/images/claude-desktop-local-mcp-servers.png +0 -0
  34. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/docs/images/codex-mcp.png +0 -0
  35. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/docs/images/cursor-mcp.png +0 -0
  36. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/docs/self-hosted.md +0 -0
  37. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/docs/tool-surface.md +0 -0
  38. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/glama.json +0 -0
  39. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/pyproject.toml +0 -0
  40. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/scripts/build_docs_index.py +0 -0
  41. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/scripts/docs_release_notes.py +0 -0
  42. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/scripts/render_server_json.py +0 -0
  43. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/server.template.json +0 -0
  44. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/__init__.py +0 -0
  45. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/__main__.py +0 -0
  46. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/apps/__init__.py +0 -0
  47. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/apps/executions.py +0 -0
  48. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/apps/files.py +0 -0
  49. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/apps/resource.py +0 -0
  50. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/apps/usage.py +0 -0
  51. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/apps/users.py +0 -0
  52. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/assets/__init__.py +0 -0
  53. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/assets/favicon.svg +0 -0
  54. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/assets/instructions.json +0 -0
  55. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/auth.py +0 -0
  56. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/catalog_policy.py +0 -0
  57. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/constants.py +0 -0
  58. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/context.py +0 -0
  59. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/docs.py +0 -0
  60. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/docs_index.py +0 -0
  61. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/docs_search.py +0 -0
  62. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/docs_source.py +0 -0
  63. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/error_classification.py +0 -0
  64. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/error_monitoring.py +0 -0
  65. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/events/__init__.py +0 -0
  66. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/events/bus.py +0 -0
  67. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/events/catalog.py +0 -0
  68. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/events/delivery.py +0 -0
  69. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/events/egress.py +0 -0
  70. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/events/envelope.py +0 -0
  71. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/events/errors.py +0 -0
  72. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/events/ingress.py +0 -0
  73. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/events/projection.py +0 -0
  74. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/events/protocol.py +0 -0
  75. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/events/streams.py +0 -0
  76. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/events/subscriptions.py +0 -0
  77. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/events/webhooks.py +0 -0
  78. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/flags.py +0 -0
  79. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/http_app.py +0 -0
  80. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/instructions.py +0 -0
  81. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/legacy.py +0 -0
  82. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/operator.py +0 -0
  83. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/service.py +0 -0
  84. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/tool_manager.py +0 -0
  85. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/e2e/fixtures/appwrite/deployment_function_duplicate.json +0 -0
  86. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/e2e/fixtures/appwrite/deployment_function_failed.json +0 -0
  87. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/e2e/fixtures/appwrite/deployment_function_ready.json +0 -0
  88. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/e2e/fixtures/appwrite/deployment_site_ready.json +0 -0
  89. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/e2e/fixtures/appwrite/execution_async_failed.json +0 -0
  90. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/e2e/fixtures/appwrite/execution_async_waiting.json +0 -0
  91. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/e2e/fixtures/appwrite/execution_sync_failed.json +0 -0
  92. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/e2e/fixtures/appwrite/file_created.json +0 -0
  93. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/e2e/fixtures/appwrite/function_activation.json +0 -0
  94. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/e2e/fixtures/appwrite/row_created.json +0 -0
  95. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/e2e/fixtures/appwrite/user_created.json +0 -0
  96. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/e2e/test_events_ingress.py +0 -0
  97. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/e2e/test_events_protocol.py +0 -0
  98. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/e2e/test_events_stream.py +0 -0
  99. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/e2e/test_events_subscribe.py +0 -0
  100. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/e2e/test_stdio.py +0 -0
  101. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/e2e/test_tools.py +0 -0
  102. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/integration/support.py +0 -0
  103. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/integration/test_avatars.py +0 -0
  104. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/integration/test_console_sdk_oauth.py +0 -0
  105. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/integration/test_docs_index.py +0 -0
  106. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/integration/test_docs_source.py +0 -0
  107. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/integration/test_functions.py +0 -0
  108. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/integration/test_locale.py +0 -0
  109. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/integration/test_messaging.py +0 -0
  110. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/integration/test_oauth_discovery.py +0 -0
  111. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/integration/test_operator.py +0 -0
  112. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/integration/test_sites.py +0 -0
  113. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/integration/test_storage.py +0 -0
  114. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/integration/test_tables_db.py +0 -0
  115. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/integration/test_teams.py +0 -0
  116. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/integration/test_users.py +0 -0
  117. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/test_all.py +0 -0
  118. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/unit/fixtures/jwe-v1.json +0 -0
  119. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/unit/test_auth.py +0 -0
  120. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/unit/test_context.py +0 -0
  121. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/unit/test_docs.py +0 -0
  122. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/unit/test_docs_search.py +0 -0
  123. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/unit/test_error_classification.py +0 -0
  124. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/unit/test_error_monitoring.py +0 -0
  125. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/unit/test_events_delivery.py +0 -0
  126. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/unit/test_events_egress.py +0 -0
  127. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/unit/test_events_envelope.py +0 -0
  128. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/unit/test_events_protocol.py +0 -0
  129. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/unit/test_http_app.py +0 -0
  130. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/unit/test_instructions.py +0 -0
  131. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/unit/test_operator.py +0 -0
  132. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/unit/test_release_metadata.py +0 -0
  133. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/unit/test_server.py +0 -0
  134. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/unit/test_service.py +0 -0
  135. {mcp_server_appwrite-2.2.0 → mcp_server_appwrite-2.2.1}/tests/unit/test_telemetry.py +0 -0
  136. {mcp_server_appwrite-2.2.0 → 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.2.0
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
@@ -48,6 +48,24 @@ text summary. 2026-07-28 clients send `clientCapabilities` with every request,
48
48
  and stdio has the handshake, so both are gated exactly. `tools/call` is not
49
49
  gated, because the view's own calls reach the server through the host.
50
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
+
51
69
  ## Security
52
70
 
53
71
  - The resource declares no CSP domains, so hosts apply the spec's restrictive
@@ -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)
@@ -29,8 +29,9 @@ from typing import Any
29
29
  import mcp.types as types
30
30
  from appwrite_console.query import Query
31
31
 
32
+ from .. import telemetry
32
33
  from ..constants import SHOW_TOOL_NAME, VIEW_WRITE_TOOL_NAME
33
- from . import executions, files, resource, usage, users
34
+ from . import executions, files, protocol, resource, usage, users
34
35
 
35
36
  Run = Callable[[dict[str, Any], bool], list[Any]]
36
37
  """Runs a hidden catalog tool from raw call arguments (``tool_name``,
@@ -86,6 +87,18 @@ class Views:
86
87
  return [(_show_tool(), self.show), (_write_tool(), self.write)]
87
88
 
88
89
  def show(self, arguments: dict[str, Any]) -> types.CallToolResult:
90
+ view = arguments.get("view")
91
+ status = "error"
92
+ try:
93
+ result = self._show(arguments)
94
+ status = "success"
95
+ return result
96
+ finally:
97
+ telemetry.record_view(
98
+ str(view) if view in View else "invalid", protocol.origin(), status
99
+ )
100
+
101
+ def _show(self, arguments: dict[str, Any]) -> types.CallToolResult:
89
102
  view = arguments.get("view")
90
103
  if view == View.TABLE:
91
104
  data = self._table(arguments)
@@ -113,6 +126,18 @@ class Views:
113
126
  )
114
127
 
115
128
  def write(self, arguments: dict[str, Any]) -> types.CallToolResult:
129
+ tool_name = arguments.get("tool_name")
130
+ status = "error"
131
+ try:
132
+ result = self._write(arguments)
133
+ status = "success"
134
+ return result
135
+ finally:
136
+ telemetry.record_view_write(
137
+ tool_name if tool_name in WRITES else "invalid", status
138
+ )
139
+
140
+ def _write(self, arguments: dict[str, Any]) -> types.CallToolResult:
116
141
  tool_name = arguments.get("tool_name")
117
142
  if tool_name not in WRITES:
118
143
  raise ValueError(
@@ -304,6 +304,8 @@ details.headers td { height: 32px; max-width: none; white-space: normal; overflo
304
304
  const PROTOCOL_VERSION = "2026-01-26";
305
305
  const SHOW_TOOL = "appwrite_show";
306
306
  const WRITE_TOOL = "appwrite_view_write";
307
+ // Tells the server's telemetry this call is the view's, not the model's.
308
+ const ORIGIN = { "io.appwrite/origin": "view" };
307
309
  const POLL_MS = 3000;
308
310
  const PAGE_SIZE = 25;
309
311
  const FILES_PAGE = 24;
@@ -352,7 +354,7 @@ details.headers td { height: 32px; max-width: none; white-space: normal; overflo
352
354
  }
353
355
 
354
356
  async function callTool(name, args) {
355
- const result = await request("tools/call", { name, arguments: args });
357
+ const result = await request("tools/call", { name, arguments: args, _meta: ORIGIN });
356
358
  if (result && result.isError) {
357
359
  throw new Error(textOf(result) || "The Appwrite call failed.");
358
360
  }
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
- name: mcp_server_appwrite-2.2.0/src/mcp_server_appwrite/data/docs_index.npz
2
+ name: mcp_server_appwrite-2.2.1/src/mcp_server_appwrite/data/docs_index.npz
3
3
  size: 35543664 bytes
4
4
  sha256: 61d266fdb9a958b272d1b351cc6ebd6e824b0d0fe294483703e0795e4b0580df
@@ -1379,6 +1379,7 @@ def build_mcp_server(
1379
1379
  ctx: ServerRequestContext, params: types.CallToolRequestParams
1380
1380
  ) -> types.CallToolResult:
1381
1381
  _emit_initialize(ctx)
1382
+ apps.bind_origin(ctx)
1382
1383
  start = time.monotonic()
1383
1384
  name = params.name
1384
1385
  arguments = params.arguments or {}
@@ -311,6 +311,20 @@ def _build_instruments(meter: Any, transport: str, version: str) -> None:
311
311
  ),
312
312
  )
313
313
 
314
+ _instruments["apps_views"] = meter.create_counter(
315
+ "mcp.apps.views",
316
+ unit="{call}",
317
+ description=(
318
+ "MCP Apps appwrite_show calls by view, origin (the model opening a "
319
+ "view, or the view refreshing and paging itself) and status."
320
+ ),
321
+ )
322
+ _instruments["apps_writes"] = meter.create_counter(
323
+ "mcp.apps.writes",
324
+ unit="{call}",
325
+ description="MCP Apps appwrite_view_write calls by action and status.",
326
+ )
327
+
314
328
  _instruments["token_usage"] = meter.create_counter(
315
329
  "mcp.token.usage",
316
330
  unit="{token}",
@@ -711,6 +725,30 @@ def record_tool_call(
711
725
  )
712
726
 
713
727
 
728
+ def record_view(view: str, origin: str, status: str) -> None:
729
+ """One ``appwrite_show`` call. ``view`` is a view name, or ``invalid``."""
730
+ _safe_add(
731
+ "apps_views",
732
+ 1,
733
+ {
734
+ "view": view,
735
+ "origin": origin,
736
+ "status": status,
737
+ "client_id": current_client_id(),
738
+ },
739
+ )
740
+
741
+
742
+ def record_view_write(action: str, status: str) -> None:
743
+ """One ``appwrite_view_write`` call. ``action`` is an allowlisted catalog
744
+ write, or ``invalid``."""
745
+ _safe_add(
746
+ "apps_writes",
747
+ 1,
748
+ {"action": action, "status": status, "client_id": current_client_id()},
749
+ )
750
+
751
+
714
752
  def record_hallucination(attempted_tool: Any) -> None:
715
753
  _safe_add(
716
754
  "tool_hallucination",
@@ -302,7 +302,7 @@ class Client:
302
302
  self, method: str, params: Mapping[str, Any] | None = None, *, path: str = "/"
303
303
  ) -> tuple[int, dict[str, Any]]:
304
304
  """One 2026-07-28 request: protocol version and method in the headers,
305
- client identity in ``params._meta``."""
305
+ client identity in ``params._meta`` next to any ``_meta`` in ``params``."""
306
306
  body = {
307
307
  "jsonrpc": "2.0",
308
308
  "id": next(self._ids),
@@ -316,6 +316,7 @@ class Client:
316
316
  "version": "1",
317
317
  },
318
318
  "io.modelcontextprotocol/clientCapabilities": self._capabilities,
319
+ **(params or {}).get("_meta", {}),
319
320
  },
320
321
  },
321
322
  }
@@ -25,6 +25,7 @@ from support import (
25
25
  Cloud,
26
26
  Project,
27
27
  Server,
28
+ collector,
28
29
  column,
29
30
  deployment,
30
31
  execution,
@@ -42,6 +43,10 @@ EXTENSION = "io.modelcontextprotocol/ui"
42
43
  MIME_TYPE = "text/html;profile=mcp-app"
43
44
  VIEW_URI = "ui://appwrite/view"
44
45
  UI = {"extensions": {EXTENSION: {"mimeTypes": [MIME_TYPE]}}}
46
+ FROM_VIEW = {"io.appwrite/origin": "view"}
47
+ """The ``_meta`` the view sends on its own calls."""
48
+ VIEWS = "mcp.apps.views"
49
+ WRITES = "mcp.apps.writes"
45
50
  ROWS = 30
46
51
  PAGE = 25
47
52
  LOG_LINES = 250
@@ -225,10 +230,16 @@ class AppsFlow(unittest.TestCase):
225
230
  cls.host.__exit__(None, None, None)
226
231
  cls.server.__exit__(None, None, None)
227
232
 
228
- def call(self, name: str, arguments: dict[str, Any]) -> dict[str, Any]:
229
- status, response = self.host.call(
230
- "tools/call", {"name": name, "arguments": arguments}
231
- )
233
+ def call(
234
+ self,
235
+ name: str,
236
+ arguments: dict[str, Any],
237
+ meta: dict[str, Any] | None = None,
238
+ ) -> dict[str, Any]:
239
+ params: dict[str, Any] = {"name": name, "arguments": arguments}
240
+ if meta is not None:
241
+ params["_meta"] = meta
242
+ status, response = self.host.call("tools/call", params)
232
243
  self.assertEqual(status, 200, response)
233
244
  return response["result"]
234
245
 
@@ -290,6 +301,9 @@ class AppsFlow(unittest.TestCase):
290
301
  # Appwrite data reaches the DOM as text only.
291
302
  self.assertNotIn("innerHTML", html)
292
303
  self.assertNotIn("insertAdjacentHTML", html)
304
+ # The view tags its own calls so telemetry can tell them apart.
305
+ self.assertIn('"io.appwrite/origin": "view"', html)
306
+ self.assertIn("_meta: ORIGIN", html)
293
307
  digest = hashlib.sha256(html.encode("utf-8")).hexdigest()[:12]
294
308
  self.assertEqual(uri, f"{VIEW_URI}/{digest}")
295
309
 
@@ -316,6 +330,8 @@ class AppsFlow(unittest.TestCase):
316
330
 
317
331
  def test_table_view_pages_through_rows(self):
318
332
  listed = cloud.calls("GET", rf"/v1/tablesdb/{DATABASE}/tables/{TABLE}/rows")
333
+ opened = collector().count(VIEWS, view="table", origin="model")
334
+ paged = collector().count(VIEWS, view="table", origin="view")
319
335
  result = self.show_table()
320
336
  data = result["structuredContent"]
321
337
  self.assertEqual(data["view"], "table")
@@ -353,9 +369,37 @@ class AppsFlow(unittest.TestCase):
353
369
  listed + 3,
354
370
  )
355
371
 
372
+ # The model opened the view; the view pages itself under its own origin.
373
+ self.call(
374
+ "appwrite_show",
375
+ {
376
+ "view": "table",
377
+ "project_id": PROJECT,
378
+ "database_id": DATABASE,
379
+ "table_id": TABLE,
380
+ "cursor": "ord_002",
381
+ },
382
+ meta=FROM_VIEW,
383
+ )
384
+ self.assertEqual(
385
+ collector().count(VIEWS, view="table", origin="view", status="success"),
386
+ paged + 1,
387
+ )
388
+ self.assertGreater(
389
+ collector().count(VIEWS, view="table", origin="model", status="success"),
390
+ opened,
391
+ )
392
+ self.assertEqual(
393
+ collector().count(VIEWS, view="table", origin="view"), paged + 1
394
+ )
395
+
356
396
  def test_view_writes_only_its_allowlist_and_only_when_confirmed(self):
357
397
  row_path = rf"/v1/tablesdb/{DATABASE}/tables/{TABLE}/rows/ord_001"
358
398
  writes = cloud.calls("PATCH", row_path)
399
+ action = "tables_db_update_row"
400
+ refused = collector().count(WRITES, action=action, status="error")
401
+ succeeded = collector().count(WRITES, action=action, status="success")
402
+ invalid = collector().count(WRITES, action="invalid")
359
403
  update = {
360
404
  "tool_name": "tables_db_update_row",
361
405
  "project_id": PROJECT,
@@ -396,6 +440,16 @@ class AppsFlow(unittest.TestCase):
396
440
  self.assertFalse(result.get("isError"), result)
397
441
  self.assertEqual(result["structuredContent"]["status"], "refunded")
398
442
  self.assertEqual(cloud.calls("PATCH", row_path), writes + 1)
443
+ # Every refusal and the write itself are counted by action; a tool off
444
+ # the allowlist is never named.
445
+ self.assertEqual(
446
+ collector().count(WRITES, action=action, status="error"), refused + 4
447
+ )
448
+ self.assertEqual(
449
+ collector().count(WRITES, action=action, status="success"), succeeded + 1
450
+ )
451
+ self.assertEqual(collector().count(WRITES, action="invalid"), invalid + 1)
452
+ self.assertEqual(collector().count(WRITES, action="tables_db_delete_row"), 0)
399
453
  last = self.show_table(cursor="ord_002")["structuredContent"]["rows"]
400
454
  self.assertEqual(last[0]["$id"], "ord_001")
401
455
  self.assertEqual(last[0]["status"], "refunded")
@@ -733,6 +787,8 @@ class AppsFlow(unittest.TestCase):
733
787
  self.assertIn("limit_history_exceeded", refused["content"][0]["text"])
734
788
 
735
789
  def test_bad_requests_are_tool_errors(self):
790
+ invalid = collector().count(VIEWS, view="invalid", status="error")
791
+ files = collector().count(VIEWS, view="files", status="error")
736
792
  cases = [
737
793
  (
738
794
  {"view": "chart"},
@@ -773,6 +829,14 @@ class AppsFlow(unittest.TestCase):
773
829
  result = self.call("appwrite_show", arguments)
774
830
  self.assertTrue(result["isError"])
775
831
  self.assertIn(error, result["content"][0]["text"])
832
+ # An unknown view name is not a label; a known one fails under its name.
833
+ self.assertEqual(
834
+ collector().count(VIEWS, view="invalid", status="error"), invalid + 1
835
+ )
836
+ self.assertEqual(collector().count(VIEWS, view="chart"), 0)
837
+ self.assertEqual(
838
+ collector().count(VIEWS, view="files", status="error"), files + 1
839
+ )
776
840
 
777
841
 
778
842
  if __name__ == "__main__":