Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/appwrite-mcp-flow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion docs/self-hosted.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions docs/tool-surface.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ flowchart LR

subgraph CAT[Internal catalog — authentication-aware]
direction LR
K[OAuth: 38 services / 981 tools<br/>API key: 26 services / 647 tools]
K[OAuth: 40 services / 1007 tools<br/>API key: 26 services / 663 tools]
end

CT -->|large output| R[(MCP resource<br/>preview + URI)]
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
14 changes: 11 additions & 3 deletions src/mcp_server_appwrite/catalog_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
{
Expand Down
5 changes: 1 addition & 4 deletions src/mcp_server_appwrite/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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}"
Expand Down
57 changes: 52 additions & 5 deletions tests/unit/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand All @@ -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)

Expand Down
8 changes: 4 additions & 4 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading