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.
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/PKG-INFO +1 -1
- mcp_server_appwrite-2.2.1/docs/apps.md +101 -0
- mcp_server_appwrite-2.2.1/src/mcp_server_appwrite/apps/executions.py +188 -0
- mcp_server_appwrite-2.2.1/src/mcp_server_appwrite/apps/files.py +210 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/apps/protocol.py +32 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/apps/resource.py +2 -1
- mcp_server_appwrite-2.2.1/src/mcp_server_appwrite/apps/usage.py +188 -0
- mcp_server_appwrite-2.2.1/src/mcp_server_appwrite/apps/users.py +168 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/apps/views.py +132 -11
- mcp_server_appwrite-2.2.1/src/mcp_server_appwrite/assets/view.html +1546 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/data/docs_index.npz +1 -1
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/server.py +1 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/telemetry.py +38 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/e2e/support.py +329 -8
- mcp_server_appwrite-2.2.1/tests/e2e/test_apps.py +843 -0
- mcp_server_appwrite-2.1.3/docs/apps.md +0 -63
- mcp_server_appwrite-2.1.3/src/mcp_server_appwrite/assets/view.html +0 -810
- mcp_server_appwrite-2.1.3/tests/e2e/test_apps.py +0 -380
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/.dockerignore +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/.env.example +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/.github/workflows/ci.yml +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/.github/workflows/production.yml +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/.github/workflows/publish.yml +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/.github/workflows/staging.yml +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/.github/workflows/update-docs-index.yml +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/.gitignore +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/.python-version +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/AGENTS.md +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/CLAUDE.md +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/Dockerfile +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/LICENSE +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/README.md +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/compose.yaml +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/docs/appwrite-mcp-flow.png +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/docs/appwrite-mcp-flow.svg +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/docs/authentication.md +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/docs/development.md +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/docs/documentation-search.md +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/docs/events.md +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/docs/flags.md +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/docs/images/claude-desktop-local-mcp-servers.png +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/docs/images/codex-mcp.png +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/docs/images/cursor-mcp.png +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/docs/self-hosted.md +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/docs/tool-surface.md +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/glama.json +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/pyproject.toml +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/scripts/build_docs_index.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/scripts/docs_release_notes.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/scripts/render_server_json.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/server.template.json +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/__init__.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/__main__.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/apps/__init__.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/assets/__init__.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/assets/favicon.svg +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/assets/instructions.json +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/auth.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/catalog_policy.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/constants.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/context.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/docs.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/docs_index.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/docs_search.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/docs_source.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/error_classification.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/error_monitoring.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/events/__init__.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/events/bus.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/events/catalog.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/events/delivery.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/events/egress.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/events/envelope.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/events/errors.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/events/ingress.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/events/projection.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/events/protocol.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/events/streams.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/events/subscriptions.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/events/webhooks.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/flags.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/http_app.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/instructions.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/legacy.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/operator.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/service.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/tool_manager.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/e2e/fixtures/appwrite/deployment_function_duplicate.json +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/e2e/fixtures/appwrite/deployment_function_failed.json +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/e2e/fixtures/appwrite/deployment_function_ready.json +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/e2e/fixtures/appwrite/deployment_site_ready.json +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/e2e/fixtures/appwrite/execution_async_failed.json +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/e2e/fixtures/appwrite/execution_async_waiting.json +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/e2e/fixtures/appwrite/execution_sync_failed.json +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/e2e/fixtures/appwrite/file_created.json +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/e2e/fixtures/appwrite/function_activation.json +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/e2e/fixtures/appwrite/row_created.json +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/e2e/fixtures/appwrite/user_created.json +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/e2e/test_events_ingress.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/e2e/test_events_protocol.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/e2e/test_events_stream.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/e2e/test_events_subscribe.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/e2e/test_stdio.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/e2e/test_tools.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/integration/support.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/integration/test_avatars.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/integration/test_console_sdk_oauth.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/integration/test_docs_index.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/integration/test_docs_source.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/integration/test_functions.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/integration/test_locale.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/integration/test_messaging.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/integration/test_oauth_discovery.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/integration/test_operator.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/integration/test_sites.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/integration/test_storage.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/integration/test_tables_db.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/integration/test_teams.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/integration/test_users.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/test_all.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/unit/fixtures/jwe-v1.json +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/unit/test_auth.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/unit/test_context.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/unit/test_docs.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/unit/test_docs_search.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/unit/test_error_classification.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/unit/test_error_monitoring.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/unit/test_events_delivery.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/unit/test_events_egress.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/unit/test_events_envelope.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/unit/test_events_protocol.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/unit/test_http_app.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/unit/test_instructions.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/unit/test_operator.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/unit/test_release_metadata.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/unit/test_server.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/unit/test_service.py +0 -0
- {mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/tests/unit/test_telemetry.py +0 -0
- {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
|
+
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)
|
{mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/apps/protocol.py
RENAMED
|
@@ -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)
|
{mcp_server_appwrite-2.1.3 → mcp_server_appwrite-2.2.1}/src/mcp_server_appwrite/apps/resource.py
RENAMED
|
@@ -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
|
|
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")),
|