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
10 changes: 10 additions & 0 deletions src/mcp_server_appwrite/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ def _resolve_server_version() -> str:
SERVER_VERSION = _resolve_server_version()
SERVER_WEBSITE_URL = "https://github.com/appwrite/mcp"
SERVER_ICON_URL = "https://mcp.appwrite.io/favicon.svg"
# MCP Registry name; the server card and server.template.json share it.
SERVER_REGISTRY_NAME = "io.github.appwrite/mcp"
# Server card description; the card schema caps it at 100 characters.
SERVER_SUMMARY = (
"Build and deploy backends: auth, Postgres/MySQL/vector databases, storage, "
"functions, hosting, CDN"
)
SERVER_CARD_SCHEMA = (
"https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json"
)

DEFAULT_ENDPOINT = "https://cloud.appwrite.io/v1"
# Region reported by single-region deployments; carries no region subdomain.
Expand Down
43 changes: 42 additions & 1 deletion src/mcp_server_appwrite/http_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
authorization-server metadata at the MCP server's own origin instead of
following ``resource_metadata`` from the 401 challenge. Mirrors the Appwrite
authorization server's discovery document verbatim.
* ``/.well-known/mcp/server-card.json`` and ``/server-card`` — SEP-2127 server
card (name, title, description, icon, remote) for pre-connection discovery.
* ``/healthz`` — liveness probe.
* ``/docs/search`` — public documentation search for the deprecated docs MCP
server, whose clients have no user to sign in. Results and limits are fixed, so
Expand Down Expand Up @@ -49,7 +51,10 @@
from mcp.server.auth.middleware.bearer_auth import AuthenticatedUser, BearerAuthBackend
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
from mcp.types import CLIENT_INFO_META_KEY, PROTOCOL_VERSION_META_KEY
from mcp_types.version import HANDSHAKE_PROTOCOL_VERSIONS
from mcp_types.version import (
HANDSHAKE_PROTOCOL_VERSIONS,
SUPPORTED_PROTOCOL_VERSIONS,
)
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.middleware.authentication import AuthenticationMiddleware
Expand Down Expand Up @@ -80,7 +85,11 @@
DOCS_DEFAULT_LIMIT,
DOCS_MAX_BODY_BYTES,
DOCS_MAX_QUERY_LENGTH,
SERVER_CARD_SCHEMA,
SERVER_REGISTRY_NAME,
SERVER_SUMMARY,
SERVER_VERSION,
SERVER_WEBSITE_URL,
)
from .docs_search import DocsSearch
from .events import protocol as events_protocol
Expand Down Expand Up @@ -505,6 +514,30 @@ async def favicon_svg_endpoint(request: Request) -> Response:
)


async def server_card_endpoint(request: Request) -> JSONResponse:
"""Server card (SEP-2127, experimental-ext-server-card v1), so registries
and agents can describe this server before connecting."""
card = {
"$schema": SERVER_CARD_SCHEMA,
"name": SERVER_REGISTRY_NAME,
"title": "Appwrite",
"description": SERVER_SUMMARY,
"version": SERVER_VERSION,
"websiteUrl": SERVER_WEBSITE_URL,
"repository": {"url": SERVER_WEBSITE_URL, "source": "github"},
"icons": [{"src": _icon_url(), "mimeType": "image/svg+xml", "sizes": ["any"]}],
"remotes": [
{
"type": "streamable-http",
"url": canonical_resource(),
"supportedProtocolVersions": list(SUPPORTED_PROTOCOL_VERSIONS),
}
],
}
headers = {**CORS_HEADERS, "Cache-Control": "public, max-age=3600"}
return JSONResponse(card, headers=headers)


async def favicon_ico_endpoint(request: Request) -> RedirectResponse:
return RedirectResponse("/favicon.svg", status_code=307)

Expand Down Expand Up @@ -595,6 +628,14 @@ async def lifespan(app: Starlette):
endpoint=oauth_authorize_proxy_endpoint,
methods=["GET"],
),
Route(
"/.well-known/mcp/server-card.json",
endpoint=server_card_endpoint,
methods=["GET", "OPTIONS"],
),
Route(
"/server-card", endpoint=server_card_endpoint, methods=["GET", "OPTIONS"]
),
Route("/favicon.svg", endpoint=favicon_svg_endpoint, methods=["GET"]),
Route("/favicon.ico", endpoint=favicon_ico_endpoint, methods=["GET"]),
Route("/healthz", endpoint=health_endpoint, methods=["GET"]),
Expand Down
25 changes: 24 additions & 1 deletion tests/e2e/test_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,17 @@
from urllib.parse import unquote

import httpx
from support import PNG, SCOPES, TOKEN, Client, Cloud, Project, Server, collector
from support import (
PNG,
PUBLIC_URL,
SCOPES,
TOKEN,
Client,
Cloud,
Project,
Server,
collector,
)

from mcp_server_appwrite import docs_search

Expand Down Expand Up @@ -146,6 +156,19 @@ def call(self, name: str, arguments: dict[str, Any]) -> dict[str, Any]:
def text(self, result: dict[str, Any]) -> str:
return result["content"][0]["text"]

def test_server_card_describes_the_server_without_auth(self):
for path in ("/.well-known/mcp/server-card.json", "/server-card"):
response = httpx.get(f"{self.server.url}{path}")
self.assertEqual(response.status_code, 200, path)
self.assertEqual(response.headers["access-control-allow-origin"], "*")
card = response.json()
self.assertEqual(card["name"], "io.github.appwrite/mcp")
self.assertEqual(card["title"], "Appwrite")
self.assertLessEqual(len(card["description"]), 100)
self.assertEqual(card["icons"][0]["src"], f"{PUBLIC_URL}/favicon.svg")
self.assertEqual(card["remotes"][0]["url"], f"{PUBLIC_URL}/")
self.assertIn("2025-11-25", card["remotes"][0]["supportedProtocolVersions"])

def test_every_tool_has_a_title_and_explicit_hints(self):
_, response = self.client.call("tools/list")
tools = {tool["name"]: tool for tool in response["result"]["tools"]}
Expand Down
Loading