From 9d357dabd03dbec55dc31f26a5547015e09abd89 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 1 Oct 2026 14:31:04 +0100 Subject: [PATCH] fix: validate zero-argument tools and upgrade console SDK to 0.7.0 --- docs/appwrite-mcp-flow.svg | 2 +- docs/self-hosted.md | 2 +- docs/tool-surface.md | 8 ++-- pyproject.toml | 2 +- src/mcp_server_appwrite/catalog_policy.py | 14 ++++-- src/mcp_server_appwrite/server.py | 5 +- tests/unit/test_server.py | 57 +++++++++++++++++++++-- uv.lock | 8 ++-- 8 files changed, 75 insertions(+), 23 deletions(-) diff --git a/docs/appwrite-mcp-flow.svg b/docs/appwrite-mcp-flow.svg index 87e35f1..2f2e26f 100644 --- a/docs/appwrite-mcp-flow.svg +++ b/docs/appwrite-mcp-flow.svg @@ -115,7 +115,7 @@ search_docs - hosted catalog · 38 services · 981 tools + hosted catalog · 40 services · 1007 tools writes need confirm_write: true diff --git a/docs/self-hosted.md b/docs/self-hosted.md index d28ae67..d2ccc8f 100644 --- a/docs/self-hosted.md +++ b/docs/self-hosted.md @@ -4,7 +4,7 @@ Running your own Appwrite instance? Run the MCP server locally over `stdio` and authenticate with a project API key instead of OAuth. The local API-key catalog is intentionally smaller than the hosted OAuth catalog: -it exposes 647 project-key-compatible methods across 26 services. DocumentsDB, +it exposes 663 project-key-compatible methods across 26 services. DocumentsDB, VectorsDB, and text embeddings are available, but console control-plane services such as organizations, domains, projects, billing, migrations, dedicated databases, usage administration, VCS administration, and WAF administration are diff --git a/docs/tool-surface.md b/docs/tool-surface.md index 824d586..a9e2a27 100644 --- a/docs/tool-surface.md +++ b/docs/tool-surface.md @@ -20,7 +20,7 @@ flowchart LR subgraph CAT[Internal catalog — authentication-aware] direction LR - K[OAuth: 38 services / 981 tools
API key: 26 services / 647 tools] + K[OAuth: 40 services / 1007 tools
API key: 26 services / 663 tools] end CT -->|large output| R[(MCP resource
preview + URI)] @@ -47,11 +47,11 @@ flowchart LR `context=organization`, or `context=project`. Hosted calls enforce the required top-level `organization_id` or `project_id` before making a request. - **Access** is still gated per-route by the scopes granted to the OAuth token. -- **Hosted OAuth** registers all 38 services and 981 methods shipped by - `appwrite-console` 0.2.1. This adds console control-plane services including +- **Hosted OAuth** registers all 40 services and 1007 methods shipped by + `appwrite-console` 0.7.0. This adds console control-plane services including projects, organizations, domains, migrations, dedicated databases, usage, VCS, vectors, WAF, notifications, and regions. -- **API-key stdio** deliberately registers only the 647 project-key-compatible +- **API-key stdio** deliberately registers only the 663 project-key-compatible methods across 26 services. It includes the new DocumentsDB, VectorsDB, and text-embeddings APIs, while console administration methods remain hidden. - **Registration** remains SDK-driven, with the authentication profile policy diff --git a/pyproject.toml b/pyproject.toml index 7dc7789..d56285e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ readme = "README.md" requires-python = ">=3.12" dependencies = [ "anyio>=4.0.0", - "appwrite-console>=0.6.0,<0.7", + "appwrite-console>=0.7.0,<0.8", "docstring-parser>=0.16", "mcp[cli]>=2,<3", "python-dotenv>=1.0.1", diff --git a/src/mcp_server_appwrite/catalog_policy.py b/src/mcp_server_appwrite/catalog_policy.py index 8dfb776..bd4d222 100644 --- a/src/mcp_server_appwrite/catalog_policy.py +++ b/src/mcp_server_appwrite/catalog_policy.py @@ -95,20 +95,28 @@ ), "functions": frozenset({"get_template", "list_templates"}), "oauth2": frozenset({"logout", "logout_post"}), + "organization": frozenset( + { + "create_ephemeral_project_key", + "create_project_key", + "delete_project_key", + "get_project_key", + "list_project_keys", + "update_project_key", + } + ), "presences": frozenset({"get_usage"}), - "project": frozenset({"get_usage"}), "sites": frozenset({"get_template", "list_templates"}), "tables_db": frozenset( { "create_migration", - "cutover_migration", + "create_cutover", "delete_migration", "get_migration", "list_migrations", "list_operations", } ), - "teams": frozenset({"list_logs"}), "users": frozenset({"get_usage"}), "vectors_db": frozenset( { diff --git a/src/mcp_server_appwrite/server.py b/src/mcp_server_appwrite/server.py index 05e0150..5b57d98 100644 --- a/src/mcp_server_appwrite/server.py +++ b/src/mcp_server_appwrite/server.py @@ -790,9 +790,6 @@ def _validate_argument_keys( ) expected_names = _expected_argument_names(tool_info) - if not expected_names: - return - unexpected_names = sorted(name for name in arguments if name not in expected_names) if not unexpected_names: return @@ -812,7 +809,7 @@ def _validate_argument_keys( hints.append(f"{name} -> {id_candidates[0]}") hint_text = f" Suggestions: {', '.join(hints)}." if hints else "" - allowed_preview = ", ".join(sorted(expected_names)) + allowed_preview = ", ".join(sorted(expected_names)) or "none" raise ValueError( f"Unsupported arguments for {tool_name}: {', '.join(unexpected_names)}. " f"Allowed arguments: {allowed_preview}.{hint_text}" diff --git a/tests/unit/test_server.py b/tests/unit/test_server.py index 9e371cd..dceb504 100644 --- a/tests/unit/test_server.py +++ b/tests/unit/test_server.py @@ -330,6 +330,31 @@ def test_coerce_enum_returns_raw_value_string(self): self.assertEqual(_coerce_argument("code", "ch", Browser), "ch") self.assertEqual(_coerce_argument("code", Browser.GOOGLE_CHROME, Browser), "ch") + def test_prepare_arguments_zero_argument_tool_rejects_extra_arguments(self): + tool_info = { + "parameter_types": {}, + "definition": types.Tool( + name="example_get", + description="Get an example.", + inputSchema={ + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": False, + }, + ), + } + + self.assertEqual(_prepare_arguments(tool_info, {}), {}) + for key in ("example_id", "exampleId", "unexpected"): + with self.subTest(key=key): + with self.assertRaises(ValueError) as caught: + _prepare_arguments(tool_info, {key: "example-123"}) + self.assertEqual( + str(caught.exception), + f"Unsupported arguments for example_get: {key}. Allowed arguments: none.", + ) + def test_prepare_arguments_accepts_camel_case_aliases(self): tool_info = { "parameter_types": { @@ -898,15 +923,27 @@ def test_register_services_returns_fresh_manager(self): {service.service_name for service in manager_a.services}, set(SERVICE_CLASSES), ) - self.assertEqual(len(manager_a.services), 39) - self.assertEqual(len(manager_a.get_all_tools()), 994) + self.assertEqual(len(manager_a.services), 40) + self.assertEqual(len(manager_a.get_all_tools()), 1007) - def test_console_sdk_0_6_surface(self): + def test_console_sdk_0_7_surface(self): manager = register_services(object(), profile=OAUTH_PROFILE) tools = manager.tools_registry self.assertIn("avatars_get_photo", tools) self.assertIn("project_update_o_auth2_hugging_face", tools) + self.assertIn("growth_create_conversation", tools) + self.assertIn("organization_create_project_key", tools) + self.assertIn("tables_db_create_cutover", tools) + for removed_tool in ( + "project_get_usage", + "projects_list_dev_keys", + "tables_db_cutover_migration", + "account_list_logs", + "teams_list_logs", + "users_list_logs", + ): + self.assertNotIn(removed_tool, tools) site_scopes = tools["sites_create"]["definition"].input_schema["properties"][ "scopes" @@ -966,7 +1003,7 @@ def test_api_key_profile_only_advertises_server_capabilities(self): tool_names = {tool.name for tool in manager.get_all_tools()} self.assertEqual(len(manager.services), 26) - self.assertEqual(len(tool_names), 652) + self.assertEqual(len(tool_names), 663) self.assertIn("avatars_get_photo", tool_names) self.assertIn("project_update_o_auth2_hugging_face", tool_names) self.assertIn("documents_db_list", tool_names) @@ -975,7 +1012,17 @@ def test_api_key_profile_only_advertises_server_capabilities(self): self.assertNotIn("domains", service_names) self.assertNotIn("organizations", service_names) self.assertNotIn("documents_db_list_operations", tool_names) - self.assertNotIn("tables_db_cutover_migration", tool_names) + self.assertNotIn("tables_db_create_cutover", tool_names) + self.assertNotIn("growth", service_names) + for method in ( + "create_ephemeral_project_key", + "create_project_key", + "delete_project_key", + "get_project_key", + "list_project_keys", + "update_project_key", + ): + self.assertNotIn(f"organization_{method}", tool_names) self.assertNotIn("affiliates", service_names) self.assertNotIn("account_list_invoices", tool_names) diff --git a/uv.lock b/uv.lock index 18fe9e0..1303a3e 100644 --- a/uv.lock +++ b/uv.lock @@ -39,15 +39,15 @@ wheels = [ [[package]] name = "appwrite-console" -version = "0.6.0" +version = "0.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f5/4e/008876bbd6ee5d41663a7a190b5ff07f3ef135ec57341fc33e18dcdbf625/appwrite_console-0.6.0.tar.gz", hash = "sha256:c406909d319d4e91463d240a3fcd9f91589fea28cb460431b1e7b4b71d6b31f8", size = 306923, upload-time = "2026-08-27T08:24:24.81Z" } +sdist = { url = "https://files.pythonhosted.org/packages/df/34/e77b210f9986d4d773a115e828c9c2170219510fe005ec846aebbadce0dc/appwrite_console-0.7.0.tar.gz", hash = "sha256:ec2f7e1dabf0214c23894db236944dc240757fd577438b4bd9ef67770cfac93e", size = 320366, upload-time = "2026-10-01T13:15:21.444Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/30/84db4c5096620ac1db9f1c6602ba49a972ce369f36f3bda0285953cab84e/appwrite_console-0.6.0-py3-none-any.whl", hash = "sha256:ae90fb5d7d89071739536d4cab48ab4563286dfdefebf803b5a67717a593ac3b", size = 536203, upload-time = "2026-08-27T08:24:23.288Z" }, + { url = "https://files.pythonhosted.org/packages/0d/0d/14746a20167e593eca77ba4b753ad4c6cc763866713f2daa392ddc185f86/appwrite_console-0.7.0-py3-none-any.whl", hash = "sha256:68bdde306ec3d10fa0486a254bd557d9efe012a58e4b7173c6dffec5a8d0edd7", size = 554751, upload-time = "2026-10-01T13:15:19.795Z" }, ] [[package]] @@ -694,7 +694,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "anyio", specifier = ">=4.0.0" }, - { name = "appwrite-console", specifier = ">=0.6.0,<0.7" }, + { name = "appwrite-console", specifier = ">=0.7.0,<0.8" }, { name = "argon2-cffi", marker = "extra == 'integration'", specifier = ">=23.1.0" }, { name = "bcrypt", marker = "extra == 'integration'", specifier = ">=4.1.2" }, { name = "docstring-parser", specifier = ">=0.16" },