From cfba6ed05c9ba03dae44f64e34f443a62bafae7b Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 11 Nov 2026 14:28:01 +0000 Subject: [PATCH 1/2] feat: serve an MCP server card at /.well-known/mcp/server-card.json Agent-readiness scanners and registries look for a SEP-1649 card at the server origin (Ora probes /server-card) to show a branded listing before connecting. mcp.appwrite.io served none, so it was reported with no icon. Serve the card, with the registry name, a schema-length description, the existing favicon and the remote URL, at both paths. --- src/mcp_server_appwrite/constants.py | 10 +++++++++ src/mcp_server_appwrite/http_app.py | 32 ++++++++++++++++++++++++++++ tests/e2e/test_tools.py | 24 ++++++++++++++++++++- 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/mcp_server_appwrite/constants.py b/src/mcp_server_appwrite/constants.py index b2309cc..4f33b7d 100644 --- a/src/mcp_server_appwrite/constants.py +++ b/src/mcp_server_appwrite/constants.py @@ -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. diff --git a/src/mcp_server_appwrite/http_app.py b/src/mcp_server_appwrite/http_app.py index 056726b..159fc2e 100644 --- a/src/mcp_server_appwrite/http_app.py +++ b/src/mcp_server_appwrite/http_app.py @@ -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-1649 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 @@ -80,7 +82,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 @@ -505,6 +511,24 @@ async def favicon_svg_endpoint(request: Request) -> Response: ) +async def server_card_endpoint(request: Request) -> JSONResponse: + """SEP-1649 server card, 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()}], + } + 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) @@ -595,6 +619,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"]), diff --git a/tests/e2e/test_tools.py b/tests/e2e/test_tools.py index 4468658..344b966 100644 --- a/tests/e2e/test_tools.py +++ b/tests/e2e/test_tools.py @@ -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 @@ -146,6 +156,18 @@ 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}/") + 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"]} From 456017d316d18013e54d3aa049f1b0f218e6e2a6 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 11 Nov 2026 14:31:26 +0000 Subject: [PATCH 2/2] feat: advertise supported protocol versions in the server card --- src/mcp_server_appwrite/http_app.py | 19 ++++++++++++++----- tests/e2e/test_tools.py | 1 + 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/mcp_server_appwrite/http_app.py b/src/mcp_server_appwrite/http_app.py index 159fc2e..eae08b0 100644 --- a/src/mcp_server_appwrite/http_app.py +++ b/src/mcp_server_appwrite/http_app.py @@ -12,7 +12,7 @@ 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-1649 server +* ``/.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 @@ -51,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 @@ -512,8 +515,8 @@ async def favicon_svg_endpoint(request: Request) -> Response: async def server_card_endpoint(request: Request) -> JSONResponse: - """SEP-1649 server card, so registries and agents can describe this server - before connecting.""" + """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, @@ -523,7 +526,13 @@ async def server_card_endpoint(request: Request) -> JSONResponse: "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()}], + "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) diff --git a/tests/e2e/test_tools.py b/tests/e2e/test_tools.py index 344b966..9f2b7f1 100644 --- a/tests/e2e/test_tools.py +++ b/tests/e2e/test_tools.py @@ -167,6 +167,7 @@ def test_server_card_describes_the_server_without_auth(self): 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")