mcp-server-appwrite 2.1.0__tar.gz → 2.1.2__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 (132) hide show
  1. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/PKG-INFO +1 -1
  2. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/src/mcp_server_appwrite/apps/resource.py +22 -1
  3. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/src/mcp_server_appwrite/apps/views.py +11 -5
  4. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/src/mcp_server_appwrite/assets/view.html +47 -2
  5. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/src/mcp_server_appwrite/data/docs_index.npz +1 -1
  6. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/src/mcp_server_appwrite/operator.py +1 -2
  7. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/e2e/test_apps.py +32 -10
  8. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/.dockerignore +0 -0
  9. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/.env.example +0 -0
  10. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/.github/workflows/ci.yml +0 -0
  11. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/.github/workflows/production.yml +0 -0
  12. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/.github/workflows/publish.yml +0 -0
  13. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/.github/workflows/staging.yml +0 -0
  14. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/.github/workflows/update-docs-index.yml +0 -0
  15. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/.gitignore +0 -0
  16. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/.python-version +0 -0
  17. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/AGENTS.md +0 -0
  18. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/CLAUDE.md +0 -0
  19. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/Dockerfile +0 -0
  20. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/LICENSE +0 -0
  21. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/README.md +0 -0
  22. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/compose.yaml +0 -0
  23. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/docs/apps.md +0 -0
  24. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/docs/appwrite-mcp-flow.png +0 -0
  25. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/docs/appwrite-mcp-flow.svg +0 -0
  26. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/docs/authentication.md +0 -0
  27. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/docs/development.md +0 -0
  28. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/docs/documentation-search.md +0 -0
  29. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/docs/events.md +0 -0
  30. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/docs/flags.md +0 -0
  31. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/docs/images/claude-desktop-local-mcp-servers.png +0 -0
  32. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/docs/images/codex-mcp.png +0 -0
  33. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/docs/images/cursor-mcp.png +0 -0
  34. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/docs/self-hosted.md +0 -0
  35. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/docs/tool-surface.md +0 -0
  36. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/glama.json +0 -0
  37. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/pyproject.toml +0 -0
  38. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/scripts/build_docs_index.py +0 -0
  39. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/scripts/docs_release_notes.py +0 -0
  40. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/scripts/render_server_json.py +0 -0
  41. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/server.template.json +0 -0
  42. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/src/mcp_server_appwrite/__init__.py +0 -0
  43. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/src/mcp_server_appwrite/__main__.py +0 -0
  44. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/src/mcp_server_appwrite/apps/__init__.py +0 -0
  45. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/src/mcp_server_appwrite/apps/protocol.py +0 -0
  46. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/src/mcp_server_appwrite/assets/__init__.py +0 -0
  47. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/src/mcp_server_appwrite/assets/favicon.svg +0 -0
  48. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/src/mcp_server_appwrite/assets/instructions.json +0 -0
  49. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/src/mcp_server_appwrite/auth.py +0 -0
  50. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/src/mcp_server_appwrite/catalog_policy.py +0 -0
  51. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/src/mcp_server_appwrite/constants.py +0 -0
  52. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/src/mcp_server_appwrite/context.py +0 -0
  53. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/src/mcp_server_appwrite/docs.py +0 -0
  54. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/src/mcp_server_appwrite/docs_index.py +0 -0
  55. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/src/mcp_server_appwrite/docs_search.py +0 -0
  56. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/src/mcp_server_appwrite/docs_source.py +0 -0
  57. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/src/mcp_server_appwrite/error_classification.py +0 -0
  58. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/src/mcp_server_appwrite/error_monitoring.py +0 -0
  59. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/src/mcp_server_appwrite/events/__init__.py +0 -0
  60. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/src/mcp_server_appwrite/events/bus.py +0 -0
  61. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/src/mcp_server_appwrite/events/catalog.py +0 -0
  62. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/src/mcp_server_appwrite/events/delivery.py +0 -0
  63. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/src/mcp_server_appwrite/events/egress.py +0 -0
  64. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/src/mcp_server_appwrite/events/envelope.py +0 -0
  65. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/src/mcp_server_appwrite/events/errors.py +0 -0
  66. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/src/mcp_server_appwrite/events/ingress.py +0 -0
  67. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/src/mcp_server_appwrite/events/projection.py +0 -0
  68. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/src/mcp_server_appwrite/events/protocol.py +0 -0
  69. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/src/mcp_server_appwrite/events/streams.py +0 -0
  70. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/src/mcp_server_appwrite/events/subscriptions.py +0 -0
  71. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/src/mcp_server_appwrite/events/webhooks.py +0 -0
  72. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/src/mcp_server_appwrite/flags.py +0 -0
  73. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/src/mcp_server_appwrite/http_app.py +0 -0
  74. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/src/mcp_server_appwrite/instructions.py +0 -0
  75. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/src/mcp_server_appwrite/legacy.py +0 -0
  76. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/src/mcp_server_appwrite/server.py +0 -0
  77. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/src/mcp_server_appwrite/service.py +0 -0
  78. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/src/mcp_server_appwrite/telemetry.py +0 -0
  79. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/src/mcp_server_appwrite/tool_manager.py +0 -0
  80. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/e2e/fixtures/appwrite/deployment_function_duplicate.json +0 -0
  81. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/e2e/fixtures/appwrite/deployment_function_failed.json +0 -0
  82. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/e2e/fixtures/appwrite/deployment_function_ready.json +0 -0
  83. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/e2e/fixtures/appwrite/deployment_site_ready.json +0 -0
  84. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/e2e/fixtures/appwrite/execution_async_failed.json +0 -0
  85. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/e2e/fixtures/appwrite/execution_async_waiting.json +0 -0
  86. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/e2e/fixtures/appwrite/execution_sync_failed.json +0 -0
  87. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/e2e/fixtures/appwrite/file_created.json +0 -0
  88. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/e2e/fixtures/appwrite/function_activation.json +0 -0
  89. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/e2e/fixtures/appwrite/row_created.json +0 -0
  90. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/e2e/fixtures/appwrite/user_created.json +0 -0
  91. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/e2e/support.py +0 -0
  92. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/e2e/test_events_ingress.py +0 -0
  93. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/e2e/test_events_protocol.py +0 -0
  94. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/e2e/test_events_stream.py +0 -0
  95. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/e2e/test_events_subscribe.py +0 -0
  96. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/e2e/test_stdio.py +0 -0
  97. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/e2e/test_tools.py +0 -0
  98. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/integration/support.py +0 -0
  99. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/integration/test_avatars.py +0 -0
  100. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/integration/test_console_sdk_oauth.py +0 -0
  101. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/integration/test_docs_index.py +0 -0
  102. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/integration/test_docs_source.py +0 -0
  103. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/integration/test_functions.py +0 -0
  104. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/integration/test_locale.py +0 -0
  105. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/integration/test_messaging.py +0 -0
  106. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/integration/test_oauth_discovery.py +0 -0
  107. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/integration/test_operator.py +0 -0
  108. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/integration/test_sites.py +0 -0
  109. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/integration/test_storage.py +0 -0
  110. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/integration/test_tables_db.py +0 -0
  111. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/integration/test_teams.py +0 -0
  112. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/integration/test_users.py +0 -0
  113. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/test_all.py +0 -0
  114. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/unit/fixtures/jwe-v1.json +0 -0
  115. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/unit/test_auth.py +0 -0
  116. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/unit/test_context.py +0 -0
  117. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/unit/test_docs.py +0 -0
  118. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/unit/test_docs_search.py +0 -0
  119. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/unit/test_error_classification.py +0 -0
  120. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/unit/test_error_monitoring.py +0 -0
  121. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/unit/test_events_delivery.py +0 -0
  122. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/unit/test_events_egress.py +0 -0
  123. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/unit/test_events_envelope.py +0 -0
  124. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/unit/test_events_protocol.py +0 -0
  125. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/unit/test_http_app.py +0 -0
  126. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/unit/test_instructions.py +0 -0
  127. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/unit/test_operator.py +0 -0
  128. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/unit/test_release_metadata.py +0 -0
  129. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/unit/test_server.py +0 -0
  130. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/unit/test_service.py +0 -0
  131. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/tests/unit/test_telemetry.py +0 -0
  132. {mcp_server_appwrite-2.1.0 → mcp_server_appwrite-2.1.2}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: mcp-server-appwrite
3
- Version: 2.1.0
3
+ Version: 2.1.2
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
@@ -2,6 +2,10 @@
2
2
 
3
3
  One HTML document (``assets/view.html``, plain JavaScript with no build step)
4
4
  renders every view; it picks one from the tool result's ``structuredContent``.
5
+ Hosts cache UI resources by URI, so the tools point at
6
+ ``ui://appwrite/view/<hash of the HTML>``: a release that changes the view
7
+ changes the URI, and hosts fetch it again. The bare ``ui://appwrite/view``
8
+ still serves the current HTML for tool lists cached before versioning.
5
9
  It declares no CSP domains, so hosts apply SEP-1865's restrictive default: no
6
10
  network access and no external scripts. Every Appwrite read and write it makes
7
11
  is a ``tools/call`` the host proxies to this server under the user's own
@@ -11,6 +15,7 @@ credentials.
11
15
  from __future__ import annotations
12
16
 
13
17
  from functools import cache
18
+ from hashlib import sha256
14
19
  from importlib.resources import files
15
20
  from typing import Any
16
21
 
@@ -23,14 +28,30 @@ from .protocol import MIME_TYPE
23
28
  META: dict[str, Any] = {"ui": {"csp": {}, "prefersBorder": True}}
24
29
 
25
30
 
31
+ VERSION_LENGTH = 12
32
+ """Hex digits of the HTML's SHA-256 in the URI: enough that two releases
33
+ never collide."""
34
+
35
+
26
36
  @cache
27
37
  def html() -> str:
28
38
  return files("mcp_server_appwrite.assets").joinpath("view.html").read_text()
29
39
 
30
40
 
41
+ @cache
42
+ def uri() -> str:
43
+ """The URI the tools point at, versioned by the HTML it serves."""
44
+ digest = sha256(html().encode("utf-8")).hexdigest()[:VERSION_LENGTH]
45
+ return f"{VIEW_URI}/{digest}"
46
+
47
+
48
+ def serves(requested: str) -> bool:
49
+ return requested in (uri(), VIEW_URI)
50
+
51
+
31
52
  def resource() -> types.Resource:
32
53
  return types.Resource(
33
- uri=VIEW_URI,
54
+ uri=uri(),
34
55
  name="Appwrite view",
35
56
  description=(
36
57
  "Interactive Appwrite view: table rows and deployment build logs."
@@ -19,6 +19,7 @@ what such a script could do.
19
19
  from __future__ import annotations
20
20
 
21
21
  import json
22
+ import re
22
23
  from collections.abc import Callable
23
24
  from enum import StrEnum
24
25
  from typing import Any
@@ -26,7 +27,8 @@ from typing import Any
26
27
  import mcp.types as types
27
28
  from appwrite_console.query import Query
28
29
 
29
- from ..constants import SHOW_TOOL_NAME, VIEW_URI, VIEW_WRITE_TOOL_NAME
30
+ from ..constants import SHOW_TOOL_NAME, VIEW_WRITE_TOOL_NAME
31
+ from . import resource
30
32
 
31
33
  Run = Callable[[dict[str, Any], bool], list[Any]]
32
34
  """Runs a hidden catalog tool from raw call arguments (``tool_name``,
@@ -46,6 +48,10 @@ SUMMARY_ROWS = 10
46
48
  SUMMARY_COLUMNS = 4
47
49
  SUMMARY_CELL = 24
48
50
 
51
+ ANSI = re.compile(r"\x1b\[[0-9;?]*[ -/]*[@-~]")
52
+ """Terminal escape sequences (colors, cursor moves) in build logs: the view
53
+ renders the colors, the model's summary drops them."""
54
+
49
55
  TERMINAL_STATUSES = frozenset({"ready", "failed", "canceled"})
50
56
 
51
57
  WRITES = (
@@ -273,10 +279,10 @@ def _show_tool() -> types.Tool:
273
279
  "additionalProperties": False,
274
280
  },
275
281
  _meta={
276
- "ui": {"resourceUri": VIEW_URI, "visibility": ["model", "app"]},
282
+ "ui": {"resourceUri": resource.uri(), "visibility": ["model", "app"]},
277
283
  # The flat key hosts read before ``ui.resourceUri``; SEP-1865 drops
278
284
  # it at GA.
279
- "ui/resourceUri": VIEW_URI,
285
+ "ui/resourceUri": resource.uri(),
280
286
  },
281
287
  )
282
288
 
@@ -302,7 +308,7 @@ def _write_tool() -> types.Tool:
302
308
  "required": ["tool_name", "arguments", "confirm_write"],
303
309
  "additionalProperties": False,
304
310
  },
305
- _meta={"ui": {"resourceUri": VIEW_URI, "visibility": ["app"]}},
311
+ _meta={"ui": {"resourceUri": resource.uri(), "visibility": ["app"]}},
306
312
  )
307
313
 
308
314
 
@@ -387,5 +393,5 @@ def _deployment_summary(data: dict[str, Any]) -> str:
387
393
  logs = deployment["logs"][-SUMMARY_LOG_LINES:]
388
394
  if logs:
389
395
  lines.append("Last build log lines:")
390
- lines.extend(logs)
396
+ lines.extend(ANSI.sub("", line) for line in logs)
391
397
  return "\n".join(lines)
@@ -183,6 +183,14 @@ pre.logs {
183
183
  .status { padding: 32px 18px; text-align: center; color: var(--color-text-secondary); }
184
184
  .status.error { color: var(--color-text-danger); text-align: left; white-space: pre-wrap; }
185
185
  :root[data-display="fullscreen"] pre.logs { height: 60vh; }
186
+ .ansi-bold { font-weight: var(--font-weight-semibold); }
187
+ .ansi-dim, .ansi-gray, .ansi-black { color: var(--color-text-secondary); }
188
+ .ansi-red { color: var(--color-text-danger); }
189
+ .ansi-green { color: var(--color-text-success); }
190
+ .ansi-yellow { color: var(--color-text-warning); }
191
+ .ansi-blue, .ansi-cyan { color: light-dark(#1d5fb4, #8ab4f8); }
192
+ .ansi-magenta { color: var(--accent); }
193
+ .ansi-white, .ansi-bright { color: var(--color-text-primary); }
186
194
  @media (max-width: 560px) {
187
195
  .fields { grid-template-columns: minmax(0, 1fr); }
188
196
  .steps { grid-template-columns: repeat(2, minmax(0, 1fr)); }
@@ -199,6 +207,11 @@ pre.logs {
199
207
  const WRITE_TOOL = "appwrite_view_write";
200
208
  const POLL_MS = 3000;
201
209
  const PAGE_SIZE = 25;
210
+ const ESCAPE = /\x1b\[[0-9;?]*[ -\/]*[@-~]/g;
211
+ const ANSI_COLORS = {
212
+ 30: "black", 31: "red", 32: "green", 33: "yellow", 34: "blue", 35: "magenta", 36: "cyan", 37: "white",
213
+ 90: "gray", 91: "red", 92: "green", 93: "yellow", 94: "blue", 95: "magenta", 96: "cyan", 97: "bright",
214
+ };
202
215
  const TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/;
203
216
  const EDITABLE = {
204
217
  string: "text", varchar: "text", text: "text", mediumtext: "text", longtext: "text",
@@ -619,6 +632,36 @@ pre.logs {
619
632
  request("ui/update-model-context", { content: [{ type: "text", text }] }).catch(() => {});
620
633
  }
621
634
 
635
+ // Build logs carry terminal color codes (SGR, "ESC[31m"). Each run of text
636
+ // becomes a span whose classes carry its color and weight; other escape
637
+ // sequences are dropped. Text only ever goes in as a text node.
638
+ function ansi(line) {
639
+ const parts = [];
640
+ const style = { color: null, bold: false, dim: false };
641
+ let last = 0;
642
+ const push = (text) => {
643
+ if (!text) return;
644
+ const classes = [style.color, style.bold && "ansi-bold", style.dim && "ansi-dim"].filter(Boolean);
645
+ parts.push(classes.length ? el("span", { className: classes.join(" ") }, text) : text);
646
+ };
647
+ for (const match of line.matchAll(ESCAPE)) {
648
+ push(line.slice(last, match.index));
649
+ last = match.index + match[0].length;
650
+ if (!match[0].endsWith("m")) continue;
651
+ const codes = match[0].slice(2, -1).split(";").map(Number);
652
+ for (const code of codes) {
653
+ if (code === 0) Object.assign(style, { color: null, bold: false, dim: false });
654
+ else if (code === 1) style.bold = true;
655
+ else if (code === 2) style.dim = true;
656
+ else if (code === 22) Object.assign(style, { bold: false, dim: false });
657
+ else if (code === 39) style.color = null;
658
+ else if (ANSI_COLORS[code]) style.color = "ansi-" + ANSI_COLORS[code];
659
+ }
660
+ }
661
+ push(line.slice(last));
662
+ return parts;
663
+ }
664
+
622
665
  function pill(status) {
623
666
  const tone = status === "ready" ? "ready"
624
667
  : status === "failed" || status === "canceled" ? "failed"
@@ -671,8 +714,10 @@ pre.logs {
671
714
  deployment.activate ? el("span", {}, "Activates when ready") : null);
672
715
 
673
716
  const logs = el("pre", { className: "logs", tabindex: "0", "aria-label": "Build logs" },
674
- (deployment.truncated ? "… earlier lines omitted\n" : "") +
675
- (deployment.logs.length ? deployment.logs.join("\n") : data.done ? "No build logs." : "Waiting for build output…"));
717
+ deployment.truncated ? el("span", { className: "ansi-dim" }, "… earlier lines omitted\n") : null,
718
+ deployment.logs.length
719
+ ? deployment.logs.flatMap((line) => [...ansi(line), "\n"])
720
+ : data.done ? "No build logs." : "Waiting for build output…");
676
721
 
677
722
  const footer = el("footer", { className: "bar" },
678
723
  el("span", { className: "muted", style: "font-size: var(--font-text-xs-size)" },
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
- name: mcp_server_appwrite-2.1.0/src/mcp_server_appwrite/data/docs_index.npz
2
+ name: mcp_server_appwrite-2.1.2/src/mcp_server_appwrite/data/docs_index.npz
3
3
  size: 35543664 bytes
4
4
  sha256: 61d266fdb9a958b272d1b351cc6ebd6e824b0d0fe294483703e0795e4b0580df
@@ -33,7 +33,6 @@ from .constants import (
33
33
  SEARCH_TOOL_NAME,
34
34
  UPDATE_HINTS,
35
35
  VERBS,
36
- VIEW_URI,
37
36
  WRITE_TOOL_NAME,
38
37
  )
39
38
  from .docs_search import DocsSearch
@@ -437,7 +436,7 @@ class Operator:
437
436
  if uri == CATALOG_URI:
438
437
  return [ReadResourceContents(self._cached_catalog_json, "application/json")]
439
438
 
440
- if apps and uri == VIEW_URI:
439
+ if apps and view_resource.serves(uri):
441
440
  return [view_resource.read()]
442
441
 
443
442
  if (
@@ -8,7 +8,9 @@ the view pages, refreshes, follows a build and saves an edit.
8
8
  checked to have reached no Appwrite write at all.
9
9
  """
10
10
 
11
+ import hashlib
11
12
  import json
13
+ import re
12
14
  import unittest
13
15
  from typing import Any
14
16
 
@@ -36,6 +38,11 @@ UI = {"extensions": {EXTENSION: {"mimeTypes": [MIME_TYPE]}}}
36
38
  ROWS = 30
37
39
  PAGE = 25
38
40
  LOG_LINES = 250
41
+ # How open-runtimes colors a build log line.
42
+ ANSI_LINE = (
43
+ "\x1b[90m[13:23:45] \x1b[31m[\x1b[0mopen-runtimes\x1b[31m] "
44
+ "\x1b[32mBuild finished. \x1b[0m"
45
+ )
39
46
 
40
47
  cloud: Cloud
41
48
 
@@ -71,7 +78,8 @@ def setUpModule() -> None:
71
78
  "dep_new",
72
79
  FUNCTION,
73
80
  "building",
74
- "\n".join(f"line {index}" for index in range(1, LOG_LINES + 1)),
81
+ "\n".join(f"line {index}" for index in range(1, LOG_LINES))
82
+ + f"\n{ANSI_LINE}",
75
83
  providerBranch="main",
76
84
  providerCommitHash="a91c3e0f00",
77
85
  ),
@@ -158,15 +166,18 @@ class AppsFlow(unittest.TestCase):
158
166
 
159
167
  listed = tools(self.host)
160
168
  show = listed["appwrite_show"]
169
+ # Hosts cache UI resources by URI, so the URI carries the HTML's hash:
170
+ # a release that changes the view makes hosts fetch it again.
171
+ uri = show["_meta"]["ui"]["resourceUri"]
172
+ self.assertRegex(uri, rf"^{re.escape(VIEW_URI)}/[0-9a-f]{{12}}$")
161
173
  self.assertEqual(
162
- show["_meta"]["ui"],
163
- {"resourceUri": VIEW_URI, "visibility": ["model", "app"]},
174
+ show["_meta"]["ui"], {"resourceUri": uri, "visibility": ["model", "app"]}
164
175
  )
165
- self.assertEqual(show["_meta"]["ui/resourceUri"], VIEW_URI)
176
+ self.assertEqual(show["_meta"]["ui/resourceUri"], uri)
166
177
  self.assertTrue(show["annotations"]["readOnlyHint"])
167
178
  write = listed["appwrite_view_write"]
168
179
  self.assertEqual(
169
- write["_meta"]["ui"], {"resourceUri": VIEW_URI, "visibility": ["app"]}
180
+ write["_meta"]["ui"], {"resourceUri": uri, "visibility": ["app"]}
170
181
  )
171
182
  self.assertTrue(write["annotations"]["destructiveHint"])
172
183
  # The operator tools are unchanged.
@@ -176,12 +187,13 @@ class AppsFlow(unittest.TestCase):
176
187
 
177
188
  _, response = self.host.call("resources/list")
178
189
  view = next(
179
- each for each in response["result"]["resources"] if each["uri"] == VIEW_URI
190
+ each for each in response["result"]["resources"] if each["uri"] == uri
180
191
  )
181
192
  self.assertEqual(view["mimeType"], MIME_TYPE)
182
193
 
183
- _, response = self.host.call("resources/read", {"uri": VIEW_URI})
194
+ _, response = self.host.call("resources/read", {"uri": uri})
184
195
  (contents,) = response["result"]["contents"]
196
+ self.assertEqual(contents["uri"], uri)
185
197
  self.assertEqual(contents["mimeType"], MIME_TYPE)
186
198
  # No CSP domains: the host's restrictive default, no network access.
187
199
  self.assertEqual(contents["_meta"]["ui"], {"csp": {}, "prefersBorder": True})
@@ -191,6 +203,12 @@ class AppsFlow(unittest.TestCase):
191
203
  # Appwrite data reaches the DOM as text only.
192
204
  self.assertNotIn("innerHTML", html)
193
205
  self.assertNotIn("insertAdjacentHTML", html)
206
+ digest = hashlib.sha256(html.encode("utf-8")).hexdigest()[:12]
207
+ self.assertEqual(uri, f"{VIEW_URI}/{digest}")
208
+
209
+ # Tool lists cached before the URI was versioned still resolve.
210
+ _, response = self.host.call("resources/read", {"uri": VIEW_URI})
211
+ self.assertEqual(response["result"]["contents"][0]["text"], html)
194
212
 
195
213
  def test_hosts_without_apps_are_not_offered_the_view(self):
196
214
  with self.server.client() as plain:
@@ -199,7 +217,7 @@ class AppsFlow(unittest.TestCase):
199
217
  self.assertNotIn("appwrite_view_write", listed)
200
218
  _, response = plain.call("resources/list")
201
219
  uris = {each["uri"] for each in response["result"]["resources"]}
202
- self.assertNotIn(VIEW_URI, uris)
220
+ self.assertFalse(any(each.startswith(VIEW_URI) for each in uris))
203
221
 
204
222
  # A 2025-era client on the stateless transport lists tools without a
205
223
  # session, so its capabilities are unknown and the view is offered.
@@ -310,12 +328,16 @@ class AppsFlow(unittest.TestCase):
310
328
  self.assertFalse(data["done"])
311
329
  # Only the end of a long log is carried.
312
330
  self.assertEqual(len(build["logs"]), 200)
313
- self.assertEqual(build["logs"][-1], f"line {LOG_LINES}")
331
+ # The view gets the colors; the model's summary gets plain text.
332
+ self.assertEqual(build["logs"][-1], ANSI_LINE)
314
333
  self.assertTrue(build["truncated"])
315
334
  text = result["content"][0]["text"]
316
335
  self.assertIn(f"Deployment dep_new of function {FUNCTION} is building.", text)
317
336
  self.assertIn("Source: main @ a91c3e0.", text)
318
- self.assertTrue(text.endswith(f"line {LOG_LINES}"))
337
+ self.assertTrue(
338
+ text.endswith("[13:23:45] [open-runtimes] Build finished. "), text
339
+ )
340
+ self.assertNotIn("\x1b", text)
319
341
 
320
342
  status_path = rf"/v1/functions/{FUNCTION}/deployments/dep_new/status"
321
343
  result = self.call(