mcp-server-appwrite 2.0.2__tar.gz → 2.1.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.0.2 → mcp_server_appwrite-2.1.1}/AGENTS.md +6 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/PKG-INFO +1 -1
- mcp_server_appwrite-2.1.1/docs/apps.md +63 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/docs/flags.md +19 -0
- mcp_server_appwrite-2.1.1/src/mcp_server_appwrite/apps/__init__.py +1 -0
- mcp_server_appwrite-2.1.1/src/mcp_server_appwrite/apps/protocol.py +80 -0
- mcp_server_appwrite-2.1.1/src/mcp_server_appwrite/apps/resource.py +45 -0
- mcp_server_appwrite-2.1.1/src/mcp_server_appwrite/apps/views.py +396 -0
- mcp_server_appwrite-2.1.1/src/mcp_server_appwrite/assets/view.html +810 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/src/mcp_server_appwrite/constants.py +3 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/src/mcp_server_appwrite/data/docs_index.npz +1 -1
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/src/mcp_server_appwrite/flags.py +10 -1
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/src/mcp_server_appwrite/operator.py +52 -13
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/src/mcp_server_appwrite/server.py +19 -9
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/e2e/support.py +188 -14
- mcp_server_appwrite-2.1.1/tests/e2e/test_apps.py +368 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/unit/test_server.py +7 -7
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/.dockerignore +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/.env.example +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/.github/workflows/ci.yml +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/.github/workflows/production.yml +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/.github/workflows/publish.yml +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/.github/workflows/staging.yml +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/.github/workflows/update-docs-index.yml +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/.gitignore +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/.python-version +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/CLAUDE.md +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/Dockerfile +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/LICENSE +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/README.md +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/compose.yaml +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/docs/appwrite-mcp-flow.png +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/docs/appwrite-mcp-flow.svg +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/docs/authentication.md +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/docs/development.md +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/docs/documentation-search.md +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/docs/events.md +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/docs/images/claude-desktop-local-mcp-servers.png +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/docs/images/codex-mcp.png +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/docs/images/cursor-mcp.png +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/docs/self-hosted.md +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/docs/tool-surface.md +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/glama.json +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/pyproject.toml +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/scripts/build_docs_index.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/scripts/docs_release_notes.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/scripts/render_server_json.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/server.template.json +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/src/mcp_server_appwrite/__init__.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/src/mcp_server_appwrite/__main__.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/src/mcp_server_appwrite/assets/__init__.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/src/mcp_server_appwrite/assets/favicon.svg +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/src/mcp_server_appwrite/assets/instructions.json +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/src/mcp_server_appwrite/auth.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/src/mcp_server_appwrite/catalog_policy.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/src/mcp_server_appwrite/context.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/src/mcp_server_appwrite/docs.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/src/mcp_server_appwrite/docs_index.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/src/mcp_server_appwrite/docs_search.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/src/mcp_server_appwrite/docs_source.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/src/mcp_server_appwrite/error_classification.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/src/mcp_server_appwrite/error_monitoring.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/src/mcp_server_appwrite/events/__init__.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/src/mcp_server_appwrite/events/bus.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/src/mcp_server_appwrite/events/catalog.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/src/mcp_server_appwrite/events/delivery.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/src/mcp_server_appwrite/events/egress.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/src/mcp_server_appwrite/events/envelope.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/src/mcp_server_appwrite/events/errors.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/src/mcp_server_appwrite/events/ingress.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/src/mcp_server_appwrite/events/projection.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/src/mcp_server_appwrite/events/protocol.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/src/mcp_server_appwrite/events/streams.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/src/mcp_server_appwrite/events/subscriptions.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/src/mcp_server_appwrite/events/webhooks.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/src/mcp_server_appwrite/http_app.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/src/mcp_server_appwrite/instructions.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/src/mcp_server_appwrite/legacy.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/src/mcp_server_appwrite/service.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/src/mcp_server_appwrite/telemetry.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/src/mcp_server_appwrite/tool_manager.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/e2e/fixtures/appwrite/deployment_function_duplicate.json +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/e2e/fixtures/appwrite/deployment_function_failed.json +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/e2e/fixtures/appwrite/deployment_function_ready.json +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/e2e/fixtures/appwrite/deployment_site_ready.json +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/e2e/fixtures/appwrite/execution_async_failed.json +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/e2e/fixtures/appwrite/execution_async_waiting.json +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/e2e/fixtures/appwrite/execution_sync_failed.json +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/e2e/fixtures/appwrite/file_created.json +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/e2e/fixtures/appwrite/function_activation.json +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/e2e/fixtures/appwrite/row_created.json +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/e2e/fixtures/appwrite/user_created.json +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/e2e/test_events_ingress.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/e2e/test_events_protocol.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/e2e/test_events_stream.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/e2e/test_events_subscribe.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/e2e/test_stdio.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/e2e/test_tools.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/integration/support.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/integration/test_avatars.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/integration/test_console_sdk_oauth.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/integration/test_docs_index.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/integration/test_docs_source.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/integration/test_functions.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/integration/test_locale.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/integration/test_messaging.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/integration/test_oauth_discovery.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/integration/test_operator.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/integration/test_sites.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/integration/test_storage.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/integration/test_tables_db.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/integration/test_teams.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/integration/test_users.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/test_all.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/unit/fixtures/jwe-v1.json +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/unit/test_auth.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/unit/test_context.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/unit/test_docs.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/unit/test_docs_search.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/unit/test_error_classification.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/unit/test_error_monitoring.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/unit/test_events_delivery.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/unit/test_events_egress.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/unit/test_events_envelope.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/unit/test_events_protocol.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/unit/test_http_app.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/unit/test_instructions.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/unit/test_operator.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/unit/test_release_metadata.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/unit/test_service.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/tests/unit/test_telemetry.py +0 -0
- {mcp_server_appwrite-2.0.2 → mcp_server_appwrite-2.1.1}/uv.lock +0 -0
|
@@ -36,6 +36,7 @@ Source lives in `src/mcp_server_appwrite/`:
|
|
|
36
36
|
| `docs_source.py` | Fetches the published docs (`/docs/llms.txt` + per-page `.md` exports on appwrite.io) and chunks them for the index build. |
|
|
37
37
|
| `docs_index.py` | Writes the index artifact deterministically and atomically: page and chunk hashes, reuse of cached chunk vectors, build lock, and the added/changed/removed page report. |
|
|
38
38
|
| `events/` | MCP Events (behind `flags.EVENTS`, HTTP only): `catalog.py` (event catalog), `protocol.py` (capability, `events/list` and the subscribe methods), `subscriptions.py` + `webhooks.py` (`events/subscribe` / `events/unsubscribe`: one managed Appwrite webhook per subscription), `envelope.py` (subscription state sealed with the `mcp-event-subscriptions` JWE profile), `egress.py` + `delivery.py` (SSRF-safe signed delivery), `projection.py` + `ingress.py` (Appwrite webhook ingress at `/appwrite/webhooks/{id}`), `streams.py` + `bus.py` (push delivery: `events/stream`, behind `flags.EVENTS_PUSH`, with events routed between replicas over Redis pub/sub). See [docs/events.md](docs/events.md). |
|
|
39
|
+
| `apps/` | MCP Apps (behind `flags.APPS`): `protocol.py` (the `io.modelcontextprotocol/ui` extension and per-client gating), `views.py` (`appwrite_show` and the view-only `appwrite_view_write`), `resource.py` (`ui://appwrite/view`, served from `assets/view.html`: plain JavaScript, no build step). See [docs/apps.md](docs/apps.md). |
|
|
39
40
|
| `telemetry.py` | OpenTelemetry metrics layer (OTLP/HTTP). No-op unless an OTLP endpoint is configured and the transport is `http`. |
|
|
40
41
|
| `data/` | Committed docs index artifact (`docs_index.npz`: vectors, chunk map, chunk hashes, embedded `meta.json`), shipped in the wheel/image. |
|
|
41
42
|
|
|
@@ -62,6 +63,11 @@ services) stays internal and is searched at runtime. API-key stdio uses a filter
|
|
|
62
63
|
tool's annotations match every call it accepts. Large outputs are stored as MCP
|
|
63
64
|
resources and returned as a preview + resource URI.
|
|
64
65
|
|
|
66
|
+
With MCP Apps on (`flags.APPS`), hosts that render them also see
|
|
67
|
+
`appwrite_show`, whose result the host renders as an interactive view
|
|
68
|
+
(`ui://appwrite/view`), and the view can call `appwrite_view_write`, which the
|
|
69
|
+
model never sees. See [docs/apps.md](docs/apps.md).
|
|
70
|
+
|
|
65
71
|
## Local development
|
|
66
72
|
|
|
67
73
|
```bash
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: mcp-server-appwrite
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.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,63 @@
|
|
|
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. 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`, only with `confirm_write=true`. |
|
|
14
|
+
| `ui://appwrite/view` | host | One HTML document (`assets/view.html`) that renders either 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). An edited row or a cancelled
|
|
20
|
+
build goes through a confirmation dialog in the view, and only then through
|
|
21
|
+
`appwrite_view_write`. A successful edit and a finished build are passed to the
|
|
22
|
+
model with `ui/update-model-context` when the host supports it.
|
|
23
|
+
|
|
24
|
+
## Who is offered the view
|
|
25
|
+
|
|
26
|
+
Hosts that declare the extension in their client capabilities get the two tools
|
|
27
|
+
and the resource. Hosts that declare capabilities without it do not. The
|
|
28
|
+
hosted transport is stateless, so a 2025-era client's `tools/list` carries no
|
|
29
|
+
capabilities; it is offered the view, and a host that cannot render it uses the
|
|
30
|
+
text summary. 2026-07-28 clients send `clientCapabilities` with every request,
|
|
31
|
+
and stdio has the handshake, so both are gated exactly. `tools/call` is not
|
|
32
|
+
gated, because the view's own calls reach the server through the host.
|
|
33
|
+
|
|
34
|
+
## Security
|
|
35
|
+
|
|
36
|
+
- The resource declares no CSP domains, so hosts apply the spec's restrictive
|
|
37
|
+
default: no network access, inline script and style only. The view never
|
|
38
|
+
holds a token; every Appwrite call is a `tools/call` the host proxies to this
|
|
39
|
+
server with the user's own credentials.
|
|
40
|
+
- Everything the view renders comes from Appwrite. It builds the DOM with
|
|
41
|
+
`textContent` and `createElement` only (the e2e flow fails on `innerHTML`),
|
|
42
|
+
because a script injected through that data could call `appwrite_view_write`.
|
|
43
|
+
The allowlist bounds what such a script could do, and `confirm_write` is
|
|
44
|
+
required as for `appwrite_call_write_tool`.
|
|
45
|
+
|
|
46
|
+
## Trying it in a real host
|
|
47
|
+
|
|
48
|
+
The ext-apps repository ships `basic-host`, the reference host. It connects
|
|
49
|
+
without auth, so point it at a local server whose requests carry a token, for
|
|
50
|
+
example the e2e harness (`tests/e2e/support.py`) with its fake Appwrite:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
git clone --depth 1 https://github.com/modelcontextprotocol/ext-apps.git
|
|
54
|
+
cd ext-apps/examples/basic-host && npm install
|
|
55
|
+
NODE_ENV=development npx cross-env INPUT=index.html vite build
|
|
56
|
+
NODE_ENV=development npx cross-env INPUT=sandbox.html vite build
|
|
57
|
+
SERVERS='["http://localhost:3001/mcp"]' bun serve.ts # host on :8080
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Pick `appwrite_show`, call it with
|
|
61
|
+
`{"view":"table","project_id":"…","database_id":"…","table_id":"…"}`, and the
|
|
62
|
+
view renders under the tool call. The host's theme toggle switches the view
|
|
63
|
+
between light and dark.
|
|
@@ -36,6 +36,7 @@ Flags are for testing overrides only — permanent configuration belongs in
|
|
|
36
36
|
| `--console-url` | `MCP_CONSOLE_URL` | Send OAuth login/consent to an alternative Appwrite Console (HTTP transport only). |
|
|
37
37
|
| `--events` | `MCP_EVENTS` | Advertise MCP Events, serve `events/list` and mount the Appwrite webhook ingress (HTTP transport only; needs `MCP_EVENTS_SEALING_KEYS`). See [events.md](events.md). |
|
|
38
38
|
| `--events-push` | `MCP_EVENTS_PUSH` | Also offer push delivery (`events/stream`) for MCP Events (needs `--events` and `MCP_EVENTS_REDIS_URL`). See [events.md](events.md#push-delivery). |
|
|
39
|
+
| `--apps` | `MCP_APPS` | Offer MCP Apps: the `appwrite_show` tool and its interactive `ui://appwrite/view`, rendered in the chat by hosts that support MCP Apps. See [apps.md](apps.md). |
|
|
39
40
|
|
|
40
41
|
### `--console-url` — test OAuth against a pre-release console
|
|
41
42
|
|
|
@@ -175,3 +176,21 @@ curl -sN http://localhost:8000/ \
|
|
|
175
176
|
|
|
176
177
|
With the flag off, `events/list` advertises webhook only and `events/stream`
|
|
177
178
|
returns `-32601` (method not found).
|
|
179
|
+
|
|
180
|
+
### `--apps` — MCP Apps
|
|
181
|
+
|
|
182
|
+
Adds `appwrite_show` (model and view), the view-only `appwrite_view_write`, and
|
|
183
|
+
the `ui://appwrite/view` resource, and advertises the
|
|
184
|
+
`io.modelcontextprotocol/ui` extension. Clients that declare their
|
|
185
|
+
capabilities without the extension are not offered them. See
|
|
186
|
+
[apps.md](apps.md) for the design and for driving it from a real host.
|
|
187
|
+
|
|
188
|
+
**Enable it:**
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
MCP_PUBLIC_URL=http://localhost:8000 \
|
|
192
|
+
uv run mcp-server-appwrite --transport http --apps 1
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
With the flag off, `tools/list` is unchanged and `resources/read` of
|
|
196
|
+
`ui://appwrite/view` is an error.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""MCP Apps: interactive Appwrite views rendered in the chat by the host."""
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""MCP Apps (SEP-1865) negotiation on the low-level SDK server.
|
|
2
|
+
|
|
3
|
+
The extension is ``io.modelcontextprotocol/ui``. A host that supports it reads
|
|
4
|
+
``_meta.ui.resourceUri`` on a tool, fetches that ``ui://`` resource with
|
|
5
|
+
``resources/read`` and renders it in a sandboxed iframe next to the tool call.
|
|
6
|
+
Hosts without it ignore ``_meta`` and show the tool's text content, so the
|
|
7
|
+
tools stay usable everywhere.
|
|
8
|
+
|
|
9
|
+
The spec asks servers to offer UI tools only to hosts that declared the
|
|
10
|
+
extension. Whether a host did is known on stdio (the ``initialize`` handshake)
|
|
11
|
+
and on the 2026-07-28 protocol (``clientCapabilities`` in every request's
|
|
12
|
+
``_meta``). The hosted 2025-era transport is stateless, so its ``tools/list``
|
|
13
|
+
arrives without either; there the tools are offered, since a host that cannot
|
|
14
|
+
render them still gets a text answer. See :func:`supported`.
|
|
15
|
+
|
|
16
|
+
Apps are offered behind the ``apps`` flag (``MCP_APPS``); see :func:`enabled`.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from collections.abc import Mapping
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from mcp.server import Server, ServerRequestContext
|
|
25
|
+
from mcp.types import CLIENT_CAPABILITIES_META_KEY
|
|
26
|
+
|
|
27
|
+
from .. import flags
|
|
28
|
+
|
|
29
|
+
EXTENSION = "io.modelcontextprotocol/ui"
|
|
30
|
+
"""SEP-1865 extension identifier under ``capabilities.extensions``."""
|
|
31
|
+
|
|
32
|
+
MIME_TYPE = "text/html;profile=mcp-app"
|
|
33
|
+
"""The only UI resource type the extension defines."""
|
|
34
|
+
|
|
35
|
+
SETTINGS: dict[str, Any] = {"mimeTypes": [MIME_TYPE]}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def enabled() -> bool:
|
|
39
|
+
"""Whether this server offers MCP Apps."""
|
|
40
|
+
return flags.enabled(flags.APPS)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def supported(context: ServerRequestContext[Any, Any]) -> bool:
|
|
44
|
+
"""Whether the host behind ``context`` may render MCP Apps: ``False`` only
|
|
45
|
+
when its capabilities are known and lack the extension."""
|
|
46
|
+
capabilities = _capabilities(context)
|
|
47
|
+
if capabilities is None:
|
|
48
|
+
return True
|
|
49
|
+
extensions = capabilities.get("extensions")
|
|
50
|
+
if not isinstance(extensions, Mapping):
|
|
51
|
+
return False
|
|
52
|
+
settings = extensions.get(EXTENSION)
|
|
53
|
+
if not isinstance(settings, Mapping):
|
|
54
|
+
return False
|
|
55
|
+
mime_types = settings.get("mimeTypes")
|
|
56
|
+
return not isinstance(mime_types, list) or MIME_TYPE in mime_types
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def register(server: Server[Any]) -> None:
|
|
60
|
+
"""Advertise the extension in ``server``'s capabilities."""
|
|
61
|
+
server.extensions[EXTENSION] = dict(SETTINGS)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _capabilities(
|
|
65
|
+
context: ServerRequestContext[Any, Any],
|
|
66
|
+
) -> Mapping[str, Any] | None:
|
|
67
|
+
"""The client capabilities of this request's ``_meta`` (2026-07-28) or of
|
|
68
|
+
the session's handshake, ``None`` when neither is known."""
|
|
69
|
+
meta = context.meta
|
|
70
|
+
if isinstance(meta, Mapping):
|
|
71
|
+
capabilities = meta.get(CLIENT_CAPABILITIES_META_KEY)
|
|
72
|
+
if isinstance(capabilities, Mapping):
|
|
73
|
+
return capabilities
|
|
74
|
+
try:
|
|
75
|
+
params = context.session.client_params
|
|
76
|
+
except Exception:
|
|
77
|
+
return None
|
|
78
|
+
if params is None:
|
|
79
|
+
return None
|
|
80
|
+
return params.capabilities.model_dump(by_alias=True, exclude_none=True)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""The Appwrite view's UI resource, ``ui://appwrite/view``.
|
|
2
|
+
|
|
3
|
+
One HTML document (``assets/view.html``, plain JavaScript with no build step)
|
|
4
|
+
renders every view; it picks one from the tool result's ``structuredContent``.
|
|
5
|
+
It declares no CSP domains, so hosts apply SEP-1865's restrictive default: no
|
|
6
|
+
network access and no external scripts. Every Appwrite read and write it makes
|
|
7
|
+
is a ``tools/call`` the host proxies to this server under the user's own
|
|
8
|
+
credentials.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from functools import cache
|
|
14
|
+
from importlib.resources import files
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
import mcp.types as types
|
|
18
|
+
from mcp.server.lowlevel.helper_types import ReadResourceContents
|
|
19
|
+
|
|
20
|
+
from ..constants import VIEW_URI
|
|
21
|
+
from .protocol import MIME_TYPE
|
|
22
|
+
|
|
23
|
+
META: dict[str, Any] = {"ui": {"csp": {}, "prefersBorder": True}}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@cache
|
|
27
|
+
def html() -> str:
|
|
28
|
+
return files("mcp_server_appwrite.assets").joinpath("view.html").read_text()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def resource() -> types.Resource:
|
|
32
|
+
return types.Resource(
|
|
33
|
+
uri=VIEW_URI,
|
|
34
|
+
name="Appwrite view",
|
|
35
|
+
description=(
|
|
36
|
+
"Interactive Appwrite view: table rows and deployment build logs."
|
|
37
|
+
),
|
|
38
|
+
mime_type=MIME_TYPE,
|
|
39
|
+
size=len(html().encode("utf-8")),
|
|
40
|
+
_meta=META,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def read() -> ReadResourceContents:
|
|
45
|
+
return ReadResourceContents(html(), MIME_TYPE, META)
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
"""The tools behind the Appwrite view (``ui://appwrite/view``).
|
|
2
|
+
|
|
3
|
+
``appwrite_show`` is the one tool the model sees. It reads what a view shows
|
|
4
|
+
(a page of a table's rows, or a Function or Site deployment with its build
|
|
5
|
+
logs) through the hidden catalog, and returns it twice: a text summary in
|
|
6
|
+
``content`` for the model and for hosts without MCP Apps, and the data in
|
|
7
|
+
``structuredContent`` for the view. The view calls it again to page, refresh
|
|
8
|
+
and follow a build, so every read it makes is one the model could make too.
|
|
9
|
+
|
|
10
|
+
``appwrite_view_write`` is visible to the view only (``visibility: ["app"]``).
|
|
11
|
+
It runs a short allowlist of catalog writes the view offers buttons for, and
|
|
12
|
+
like ``appwrite_call_write_tool`` it refuses to run without
|
|
13
|
+
``confirm_write=true``, which the view sends only after the user confirms the
|
|
14
|
+
change in a dialog. Anything the view renders comes from Appwrite, so a
|
|
15
|
+
script injected through that data could call this tool; the allowlist bounds
|
|
16
|
+
what such a script could do.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import re
|
|
23
|
+
from collections.abc import Callable
|
|
24
|
+
from enum import StrEnum
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
import mcp.types as types
|
|
28
|
+
from appwrite_console.query import Query
|
|
29
|
+
|
|
30
|
+
from ..constants import SHOW_TOOL_NAME, VIEW_URI, VIEW_WRITE_TOOL_NAME
|
|
31
|
+
|
|
32
|
+
Run = Callable[[dict[str, Any], bool], list[Any]]
|
|
33
|
+
"""Runs a hidden catalog tool from raw call arguments (``tool_name``,
|
|
34
|
+
``arguments``, ``project_id``, ``confirm_write``), as a write or a read."""
|
|
35
|
+
|
|
36
|
+
Handler = Callable[[dict[str, Any]], types.CallToolResult]
|
|
37
|
+
|
|
38
|
+
PAGE_SIZE = 25
|
|
39
|
+
"""Rows per page: Appwrite's default list limit."""
|
|
40
|
+
|
|
41
|
+
LOG_LINES = 200
|
|
42
|
+
"""Build log lines a view carries. Hosts may put ``structuredContent`` in the
|
|
43
|
+
model's context, and a build log can run to megabytes."""
|
|
44
|
+
|
|
45
|
+
SUMMARY_LOG_LINES = 20
|
|
46
|
+
SUMMARY_ROWS = 10
|
|
47
|
+
SUMMARY_COLUMNS = 4
|
|
48
|
+
SUMMARY_CELL = 24
|
|
49
|
+
|
|
50
|
+
ANSI = re.compile(r"\x1b\[[0-9;?]*[ -/]*[@-~]")
|
|
51
|
+
"""Terminal escape sequences (colors, cursor moves) in build logs: the view
|
|
52
|
+
renders the colors, the model's summary drops them."""
|
|
53
|
+
|
|
54
|
+
TERMINAL_STATUSES = frozenset({"ready", "failed", "canceled"})
|
|
55
|
+
|
|
56
|
+
WRITES = (
|
|
57
|
+
"tables_db_update_row",
|
|
58
|
+
"functions_update_deployment_status",
|
|
59
|
+
"sites_update_deployment_status",
|
|
60
|
+
)
|
|
61
|
+
"""The writes the view offers: editing a row and cancelling a build."""
|
|
62
|
+
|
|
63
|
+
_SHOW_TITLE = "Show Appwrite view"
|
|
64
|
+
_WRITE_TITLE = "Run Appwrite view action"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class View(StrEnum):
|
|
68
|
+
TABLE = "table"
|
|
69
|
+
DEPLOYMENT = "deployment"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Views:
|
|
73
|
+
def __init__(self, run: Run):
|
|
74
|
+
self._run = run
|
|
75
|
+
|
|
76
|
+
def tools(self) -> list[tuple[types.Tool, Handler]]:
|
|
77
|
+
return [(_show_tool(), self.show), (_write_tool(), self.write)]
|
|
78
|
+
|
|
79
|
+
def show(self, arguments: dict[str, Any]) -> types.CallToolResult:
|
|
80
|
+
view = arguments.get("view")
|
|
81
|
+
if view == View.TABLE:
|
|
82
|
+
data = self._table(arguments)
|
|
83
|
+
text = _table_summary(data)
|
|
84
|
+
elif view == View.DEPLOYMENT:
|
|
85
|
+
data = self._deployment(arguments)
|
|
86
|
+
text = _deployment_summary(data)
|
|
87
|
+
else:
|
|
88
|
+
raise ValueError(f"view must be one of: {', '.join(View)}. Got {view!r}.")
|
|
89
|
+
return types.CallToolResult(
|
|
90
|
+
content=[types.TextContent(type="text", text=text)],
|
|
91
|
+
structured_content=data,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def write(self, arguments: dict[str, Any]) -> types.CallToolResult:
|
|
95
|
+
tool_name = arguments.get("tool_name")
|
|
96
|
+
if tool_name not in WRITES:
|
|
97
|
+
raise ValueError(
|
|
98
|
+
f"{VIEW_WRITE_TOOL_NAME} runs only {', '.join(WRITES)}. "
|
|
99
|
+
f"Got {tool_name!r}."
|
|
100
|
+
)
|
|
101
|
+
# Only the literal true confirms: a string such as "false" is truthy.
|
|
102
|
+
if arguments.get("confirm_write") is not True:
|
|
103
|
+
raise ValueError(
|
|
104
|
+
f"{VIEW_WRITE_TOOL_NAME} runs {tool_name} only with confirm_write=true."
|
|
105
|
+
)
|
|
106
|
+
result = _json(self._run(arguments, True))
|
|
107
|
+
if isinstance(result, dict) and tool_name == "tables_db_update_row":
|
|
108
|
+
result = _row(result)
|
|
109
|
+
return types.CallToolResult(
|
|
110
|
+
content=[types.TextContent(type="text", text=f"{tool_name} succeeded.")],
|
|
111
|
+
structured_content=result if isinstance(result, dict) else None,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def _read(
|
|
115
|
+
self, tool_name: str, project_id: Any, arguments: dict[str, Any]
|
|
116
|
+
) -> dict[str, Any]:
|
|
117
|
+
result = _json(
|
|
118
|
+
self._run(
|
|
119
|
+
{
|
|
120
|
+
"tool_name": tool_name,
|
|
121
|
+
"project_id": project_id,
|
|
122
|
+
"arguments": arguments,
|
|
123
|
+
},
|
|
124
|
+
False,
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
if not isinstance(result, dict):
|
|
128
|
+
raise ValueError(f"{tool_name} returned an unexpected response.")
|
|
129
|
+
return result
|
|
130
|
+
|
|
131
|
+
def _table(self, arguments: dict[str, Any]) -> dict[str, Any]:
|
|
132
|
+
project_id = arguments.get("project_id")
|
|
133
|
+
database_id = _required(arguments, "database_id")
|
|
134
|
+
table_id = _required(arguments, "table_id")
|
|
135
|
+
cursor = arguments.get("cursor")
|
|
136
|
+
target = {"database_id": database_id, "table_id": table_id}
|
|
137
|
+
|
|
138
|
+
table = self._read("tables_db_get_table", project_id, target)
|
|
139
|
+
# One row past the page tells whether another page follows.
|
|
140
|
+
queries = [Query.order_desc("$createdAt"), Query.limit(PAGE_SIZE + 1)]
|
|
141
|
+
if cursor:
|
|
142
|
+
queries.append(Query.cursor_after(str(cursor)))
|
|
143
|
+
page = self._read(
|
|
144
|
+
"tables_db_list_rows", project_id, {**target, "queries": queries}
|
|
145
|
+
)
|
|
146
|
+
rows = [_row(each) for each in page.get("rows") or []]
|
|
147
|
+
more = len(rows) > PAGE_SIZE
|
|
148
|
+
rows = rows[:PAGE_SIZE]
|
|
149
|
+
columns = [
|
|
150
|
+
{"key": column.get("key"), "type": column.get("type")}
|
|
151
|
+
for column in table.get("columns") or []
|
|
152
|
+
if column.get("status", "available") == "available"
|
|
153
|
+
]
|
|
154
|
+
return {
|
|
155
|
+
"view": View.TABLE.value,
|
|
156
|
+
"projectId": project_id,
|
|
157
|
+
"databaseId": database_id,
|
|
158
|
+
"tableId": table_id,
|
|
159
|
+
"name": table.get("name") or table_id,
|
|
160
|
+
"columns": columns,
|
|
161
|
+
"rows": rows,
|
|
162
|
+
"total": int(page.get("total", len(rows))),
|
|
163
|
+
"cursor": cursor,
|
|
164
|
+
"next": rows[-1].get("$id") if more else None,
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
def _deployment(self, arguments: dict[str, Any]) -> dict[str, Any]:
|
|
168
|
+
project_id = arguments.get("project_id")
|
|
169
|
+
function_id = arguments.get("function_id")
|
|
170
|
+
site_id = arguments.get("site_id")
|
|
171
|
+
if bool(function_id) == bool(site_id):
|
|
172
|
+
raise ValueError("Pass exactly one of function_id or site_id.")
|
|
173
|
+
kind, resource_key, resource_id = (
|
|
174
|
+
("function", "function_id", function_id)
|
|
175
|
+
if function_id
|
|
176
|
+
else ("site", "site_id", site_id)
|
|
177
|
+
)
|
|
178
|
+
service = "functions" if function_id else "sites"
|
|
179
|
+
target = {resource_key: resource_id}
|
|
180
|
+
|
|
181
|
+
deployment_id = arguments.get("deployment_id")
|
|
182
|
+
if deployment_id:
|
|
183
|
+
deployment = self._read(
|
|
184
|
+
f"{service}_get_deployment",
|
|
185
|
+
project_id,
|
|
186
|
+
{**target, "deployment_id": deployment_id},
|
|
187
|
+
)
|
|
188
|
+
else:
|
|
189
|
+
latest = self._read(
|
|
190
|
+
f"{service}_list_deployments",
|
|
191
|
+
project_id,
|
|
192
|
+
{
|
|
193
|
+
**target,
|
|
194
|
+
"queries": [Query.order_desc("$createdAt"), Query.limit(1)],
|
|
195
|
+
},
|
|
196
|
+
)
|
|
197
|
+
deployments = latest.get("deployments") or []
|
|
198
|
+
if not deployments:
|
|
199
|
+
raise ValueError(f"The {kind} {resource_id} has no deployments.")
|
|
200
|
+
deployment = deployments[0]
|
|
201
|
+
|
|
202
|
+
logs = (deployment.get("buildLogs") or "").splitlines()
|
|
203
|
+
status = deployment.get("status") or "unknown"
|
|
204
|
+
return {
|
|
205
|
+
"view": View.DEPLOYMENT.value,
|
|
206
|
+
"projectId": project_id,
|
|
207
|
+
"kind": kind,
|
|
208
|
+
"resourceId": resource_id,
|
|
209
|
+
"deployment": {
|
|
210
|
+
"id": deployment.get("$id"),
|
|
211
|
+
"status": status,
|
|
212
|
+
"createdAt": deployment.get("$createdAt"),
|
|
213
|
+
"updatedAt": deployment.get("$updatedAt"),
|
|
214
|
+
"buildDuration": deployment.get("buildDuration"),
|
|
215
|
+
"sourceSize": deployment.get("sourceSize"),
|
|
216
|
+
"buildSize": deployment.get("buildSize"),
|
|
217
|
+
"activate": deployment.get("activate"),
|
|
218
|
+
"branch": deployment.get("providerBranch") or None,
|
|
219
|
+
"commit": deployment.get("providerCommitHash") or None,
|
|
220
|
+
"commitMessage": deployment.get("providerCommitMessage") or None,
|
|
221
|
+
"logs": logs[-LOG_LINES:],
|
|
222
|
+
"truncated": len(logs) > LOG_LINES,
|
|
223
|
+
},
|
|
224
|
+
"done": status in TERMINAL_STATUSES,
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _show_tool() -> types.Tool:
|
|
229
|
+
return types.Tool(
|
|
230
|
+
name=SHOW_TOOL_NAME,
|
|
231
|
+
title=_SHOW_TITLE,
|
|
232
|
+
annotations=_annotations(_SHOW_TITLE, read_only=True),
|
|
233
|
+
description=(
|
|
234
|
+
"Show an interactive Appwrite view in the chat: view=table pages "
|
|
235
|
+
"through a table's rows and lets the user edit one; view=deployment "
|
|
236
|
+
"follows a Function or Site deployment's status and build logs live. "
|
|
237
|
+
"Use it when the user wants to see or browse this data rather than "
|
|
238
|
+
"reason over it; the result also carries a short text summary. Calls "
|
|
239
|
+
"the Appwrite REST API."
|
|
240
|
+
),
|
|
241
|
+
input_schema={
|
|
242
|
+
"type": "object",
|
|
243
|
+
"properties": {
|
|
244
|
+
"view": {"type": "string", "enum": [view.value for view in View]},
|
|
245
|
+
"project_id": {
|
|
246
|
+
"type": "string",
|
|
247
|
+
"description": "Appwrite project ID the data lives in.",
|
|
248
|
+
},
|
|
249
|
+
"database_id": {
|
|
250
|
+
"type": "string",
|
|
251
|
+
"description": "view=table: the TablesDB database ID.",
|
|
252
|
+
},
|
|
253
|
+
"table_id": {
|
|
254
|
+
"type": "string",
|
|
255
|
+
"description": "view=table: the table ID.",
|
|
256
|
+
},
|
|
257
|
+
"cursor": {
|
|
258
|
+
"type": "string",
|
|
259
|
+
"description": (
|
|
260
|
+
"view=table: continue after this row ID (the previous "
|
|
261
|
+
"page's `next`). Omit for the newest rows."
|
|
262
|
+
),
|
|
263
|
+
},
|
|
264
|
+
"function_id": {
|
|
265
|
+
"type": "string",
|
|
266
|
+
"description": "view=deployment: the Function ID (or site_id).",
|
|
267
|
+
},
|
|
268
|
+
"site_id": {
|
|
269
|
+
"type": "string",
|
|
270
|
+
"description": "view=deployment: the Site ID (or function_id).",
|
|
271
|
+
},
|
|
272
|
+
"deployment_id": {
|
|
273
|
+
"type": "string",
|
|
274
|
+
"description": "view=deployment: omit for the latest deployment.",
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
"required": ["view"],
|
|
278
|
+
"additionalProperties": False,
|
|
279
|
+
},
|
|
280
|
+
_meta={
|
|
281
|
+
"ui": {"resourceUri": VIEW_URI, "visibility": ["model", "app"]},
|
|
282
|
+
# The flat key hosts read before ``ui.resourceUri``; SEP-1865 drops
|
|
283
|
+
# it at GA.
|
|
284
|
+
"ui/resourceUri": VIEW_URI,
|
|
285
|
+
},
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _write_tool() -> types.Tool:
|
|
290
|
+
return types.Tool(
|
|
291
|
+
name=VIEW_WRITE_TOOL_NAME,
|
|
292
|
+
title=_WRITE_TITLE,
|
|
293
|
+
annotations=_annotations(_WRITE_TITLE, read_only=False),
|
|
294
|
+
description=(
|
|
295
|
+
"Used by the Appwrite view after the user confirms a change in it. "
|
|
296
|
+
"Runs one of a fixed set of Appwrite writes; requires "
|
|
297
|
+
"confirm_write=true. Calls the Appwrite REST API."
|
|
298
|
+
),
|
|
299
|
+
input_schema={
|
|
300
|
+
"type": "object",
|
|
301
|
+
"properties": {
|
|
302
|
+
"tool_name": {"type": "string", "enum": list(WRITES)},
|
|
303
|
+
"arguments": {"type": "object"},
|
|
304
|
+
"project_id": {"type": "string"},
|
|
305
|
+
"confirm_write": {"type": "boolean"},
|
|
306
|
+
},
|
|
307
|
+
"required": ["tool_name", "arguments", "confirm_write"],
|
|
308
|
+
"additionalProperties": False,
|
|
309
|
+
},
|
|
310
|
+
_meta={"ui": {"resourceUri": VIEW_URI, "visibility": ["app"]}},
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _annotations(title: str, *, read_only: bool) -> types.ToolAnnotations:
|
|
315
|
+
return types.ToolAnnotations(
|
|
316
|
+
title=title,
|
|
317
|
+
read_only_hint=read_only,
|
|
318
|
+
destructive_hint=not read_only,
|
|
319
|
+
idempotent_hint=read_only,
|
|
320
|
+
open_world_hint=True,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _required(arguments: dict[str, Any], key: str) -> str:
|
|
325
|
+
value = arguments.get(key)
|
|
326
|
+
if not isinstance(value, str) or not value:
|
|
327
|
+
raise ValueError(f"{key} is required for view={arguments.get('view')}.")
|
|
328
|
+
return value
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _row(row: dict[str, Any]) -> dict[str, Any]:
|
|
332
|
+
"""A row with its column values beside the system fields. The SDK's ``Row``
|
|
333
|
+
model moves column values into ``data``."""
|
|
334
|
+
values = row.get("data")
|
|
335
|
+
if not isinstance(values, dict):
|
|
336
|
+
return row
|
|
337
|
+
return {key: value for key, value in row.items() if key != "data"} | values
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _json(content: list[Any]) -> Any:
|
|
341
|
+
text = "\n".join(
|
|
342
|
+
item.text for item in content if isinstance(item, types.TextContent)
|
|
343
|
+
)
|
|
344
|
+
try:
|
|
345
|
+
return json.loads(text)
|
|
346
|
+
except ValueError as error:
|
|
347
|
+
raise ValueError("Appwrite returned a response that is not JSON.") from error
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _cell(value: Any) -> str:
|
|
351
|
+
text = value if isinstance(value, str) else json.dumps(value, default=str)
|
|
352
|
+
text = text.replace("\n", " ")
|
|
353
|
+
if len(text) > SUMMARY_CELL:
|
|
354
|
+
return text[: SUMMARY_CELL - 1] + "…"
|
|
355
|
+
return text
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _table_summary(data: dict[str, Any]) -> str:
|
|
359
|
+
rows: list[dict[str, Any]] = data["rows"]
|
|
360
|
+
keys = ["$id"] + [
|
|
361
|
+
column["key"] for column in data["columns"][: SUMMARY_COLUMNS - 1]
|
|
362
|
+
]
|
|
363
|
+
lines = [
|
|
364
|
+
f"Table {data['name']} ({data['databaseId']}/{data['tableId']}): "
|
|
365
|
+
f"{data['total']} rows, showing {len(rows)} newest"
|
|
366
|
+
+ (" after the cursor." if data["cursor"] else ".")
|
|
367
|
+
]
|
|
368
|
+
if rows:
|
|
369
|
+
lines.append(" | ".join(keys))
|
|
370
|
+
for row in rows[:SUMMARY_ROWS]:
|
|
371
|
+
lines.append(" | ".join(_cell(row.get(key, "")) for key in keys))
|
|
372
|
+
if len(rows) > SUMMARY_ROWS:
|
|
373
|
+
lines.append(f"… {len(rows) - SUMMARY_ROWS} more rows shown in the view.")
|
|
374
|
+
if data["next"]:
|
|
375
|
+
lines.append(
|
|
376
|
+
f"Next page: {SHOW_TOOL_NAME} with view=table and cursor={data['next']}."
|
|
377
|
+
)
|
|
378
|
+
return "\n".join(lines)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _deployment_summary(data: dict[str, Any]) -> str:
|
|
382
|
+
deployment = data["deployment"]
|
|
383
|
+
lines = [
|
|
384
|
+
f"Deployment {deployment['id']} of {data['kind']} {data['resourceId']} "
|
|
385
|
+
f"is {deployment['status']}."
|
|
386
|
+
]
|
|
387
|
+
if deployment["commit"]:
|
|
388
|
+
lines.append(
|
|
389
|
+
f"Source: {deployment['branch'] or 'unknown branch'} @ "
|
|
390
|
+
f"{deployment['commit'][:7]}."
|
|
391
|
+
)
|
|
392
|
+
logs = deployment["logs"][-SUMMARY_LOG_LINES:]
|
|
393
|
+
if logs:
|
|
394
|
+
lines.append("Last build log lines:")
|
|
395
|
+
lines.extend(ANSI.sub("", line) for line in logs)
|
|
396
|
+
return "\n".join(lines)
|