{"slug": "how-i-built-the-appwrite-mcp-server-and-decided-to-hide-most-of-its-capabilities", "title": "How I built the Appwrite MCP server (and decided to hide most of its capabilities)", "summary": "Appwrite's engineering team, led by Christy and including intern Matej, built an MCP server that initially supported only stdio transport, then added a hosted version with OAuth-based authorization. The team implemented RFC 9728, RFC 8707, and RFC 7591 to secure the server, but discovered that clients interpreted the specs differently, requiring a logging proxy to debug. They also chose to skip session management, which proved correct as the protocol later removed sessions.", "body_md": "When Anthropic introduced the Model Context Protocol on November 25, 2024, it got everyone's eyes on it, including Christy, who was Appwrite's Engineering Lead back then. I had just started my role as an \"Engineering Intern\" and had no idea what a whole new protocol meant, or why it was such a big deal.\n\nLooking at the surface, I wasn't entirely wrong. MCP is JSON-RPC with a schema and a handshake stapled on. What took us sixteen months was everything stapled around it.\n\n*Streamable HTTP did not exist when MCP launched. It replaced HTTP+SSE in the 2025-03-26 revision.*\n\nChristy had a working stdio server in the repo by February 26, 2025. We already had API keys, so the wiring was simple:\n\n```\nclaude mcp add appwrite \\\n  --env APPWRITE_PROJECT_ID=<YOUR_PROJECT_ID> \\\n  --env APPWRITE_API_KEY=<YOUR_API_KEY> \\\n  --env APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1 \\\n  -- uvx mcp-server-appwrite\n```\n\nAn API key is scoped to exactly one project by design, so the ceiling was baked into the credential. Switching projects meant editing your editor config. Creating a project was impossible. So was anything at the organization level.\n\nThe credential is the whole difference between the two transports, and everything hard about the hosted version follows from swapping it for a token that belongs to the user instead of the project.\n\nBy the spec, authorization is genuinely optional:\n\nAuthorization is OPTIONAL for MCP implementations. [...] Implementations using an HTTP-based transport SHOULD conform to this specification.\n\nFor a service where one tool call can drop a database, we weren't comfortable treating it as optional. If you use Auth0 or WorkOS, this is a config screen. Appwrite keeps everything in-house, so Matej built the authorization server itself, and I built the resource server plus whatever Cloud was still missing before real clients would work.\n\n*Steps 2 through 6 are the part that makes \"just paste this URL\" work. Nothing is pre-provisioned.*\n\nThree RFCs carry that flow. Protected Resource Metadata (RFC 9728) is the only real MUST in the whole authorization spec:\n\n```\n{\n  \"resource\": \"https://mcp.appwrite.io/\",\n  \"authorization_servers\": [\"https://cloud.appwrite.io/v1/oauth2/console\"],\n  \"scopes_supported\": [\"...\"],\n  \"bearer_methods_supported\": [\"header\"]\n}\n```\n\nResource Indicators (RFC 8707) put our canonical URI into the token's `aud`\n\n, so a token minted for another service can't be replayed against us. Dynamic Client Registration (RFC 7591) is what lets a client self-register. Add PKCE with `S256`\n\n, RFC 8414 discovery, and you have the shape of it.\n\nThe RFCs are documented. What isn't documented is that every client reads them differently, and you find out in production:\n\n`2025-03-26`\n\nauthorization spec, which looks for `/.well-known/oauth-authorization-server`\n\ninstead of the protected-resource route. I only found it by putting a logging proxy in front of the server and watching what it actually asked for.`127.0.0.1`\n\n. We weren't.`scope`\n\nparameter of ~2,680 characters against a validator capped at 2,048. Nobody ever reached a consent screen.One warning if you're about to build this: RFC 7591 went from SHOULD to MAY in `2025-11-25`\n\nand is deprecated as of `2026-07-28`\n\n, replaced by Client ID Metadata Documents. We shipped that too. This part of the spec is still moving.\n\nThe `2025-06-18`\n\nspec let a server hand out an `Mcp-Session-Id`\n\nalongside the `InitializeResult`\n\n, with `DELETE`\n\nto terminate and `Last-Event-ID`\n\nfor resumability. We skipped all of it:\n\n```\nStreamableHTTPSessionManager(app=server, json_response=False, stateless=True)\n```\n\nEvery request carries a bearer token. Verify it, build a client from it, serve the call. Nothing to store, nothing to lose on restart, nothing to make sticky across replicas.\n\nThat turned out to be the right bet for a reason I can take no credit for. The `2026-07-28`\n\nrevision removed sessions from the protocol entirely. `Mcp-Session-Id`\n\n, the `initialize`\n\nhandshake, the GET SSE stream: all gone. What we do carry is version negotiation, because you don't get to pick your clients' protocol version.\n\nMany intellectuals like myself must have wondered why MCP exists at all. Can't this be 100x simpler with, I don't know, REST?\n\nThe model only knows its training data plus whatever you hand it at runtime. If it has never seen Appwrite, it will never guess this:\n\n```\nPOST https://<REGION>.cloud.appwrite.io/v1/tablesdb\nX-Appwrite-Project: <PROJECT_ID>\nX-Appwrite-Key: <API_KEY>\nContent-Type: application/json\n\n{ \"databaseId\": \"unique()\", \"name\": \"Production\" }\n```\n\nThe endpoint, the header names, the fact that `unique()`\n\nis a magic value. With MCP the same operation shows up self-describing:\n\n```\n{\n  \"name\": \"tables_db_create\",\n  \"description\": \"Create a database in an Appwrite project\",\n  \"inputSchema\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"databaseId\": { \"type\": \"string\" },\n      \"name\": { \"type\": \"string\" }\n    },\n    \"required\": [\"databaseId\", \"name\"]\n  }\n}\n```\n\nYou can be happy knowing AI needs a lot more handholding than you do (for now).\n\nEvery MCP server I looked at ships a small, curated set. Appwrite generates one tool per SDK method, which lands at 981 methods across 38 services.\n\n*Counts as of August 2026. GitHub's 90 are grouped into 22 toolsets with 5 on by default.*\n\nThere's no version of \"expose them all\" that works, for two unrelated reasons.\n\n**The clients won't take them.** In early 2025 Cursor documented that it \"will only send the first 40 tools to the Agent\" and truncated silently. Windsurf refused outright above 50.\n\n*Discord, liviu74, Mar 14 2025. Windsurf refused it; Cursor accepted it and silently dropped tools 41 onward.*\n\nThat was with per-service flags already in place, which is the part that stings. A community user opened [issue #17](https://github.com/appwrite/mcp/issues/17), \"Please reduce the number of tools\":\n\nCursor has 40 MCP tools limit to use, but Appwrite solely has 195 tools, so it cannot be used with other tools nor even all of Appwrite tools.\n\nI pointed out you could narrow it with `--databases`\n\n. The reply:\n\nThat's quite non-sense. Then do I have to edit MCP parameter settings for each time whenever I do another jobs...? And anyway,\n\n`--databases`\n\nsolely has 42 tools, which already exceeds Cursor's recommended limit (40).\n\n**Quality falls off well below the caps.** The numbers converge from unrelated directions. Anthropic puts degradation at \"once you exceed 30-50 available tools\". OpenAI says \"fewer than 20 functions at the start of a turn\". Block's Goose recommends 50 or fewer. And the fix measures well: Anthropic's [advanced tool use](https://www.anthropic.com/engineering/advanced-tool-use) work takes Opus 4 from 49% to 74% on MCP tool-use evals with a search tool enabled, and Opus 4.5 from 79.5% to 88.1%, with 85% fewer tokens on definitions. [RAG-MCP](https://arxiv.org/abs/2505.03275) more than triples selection accuracy (43.13% against 13.62%).\n\nThe caveat, because it cuts against me: [MCPVerse](https://arxiv.org/abs/2508.16260) found some agentic models handle big action spaces fine. Claude-4-Sonnet scored 62.3 with an oracle tool set and 62.4 with ~220 tools. Big catalogs aren't fatal. They're a tax you're paying for nothing when the agent needs three tools out of 981.\n\n`appwrite_get_context`\n\nanswers where you are and which projects you can see`appwrite_search_tools`\n\nsearches the hidden catalog in natural language`appwrite_call_tool`\n\ncalls one of them by name`appwrite_search_docs`\n\nsearches the Appwrite docs semanticallySearch narrows at request time, which is why the per-service flags could be deleted entirely. Mutations require `confirm_write: true`\n\n, and results too large for the conversation become MCP resources instead.\n\nThe scoring behind `appwrite_search_tools`\n\nis deliberately dumb: token and substring matching against the tool name, description, service and resource, a bonus when the query's inferred verb matches the tool's, a penalty when it doesn't. No embeddings, no index to rebuild, no inference call in the hot path.\n\nHere's what a client sees:\n\n*981 methods behind 4 tools and 1 resource. The \"Logout\" link is the OAuth session.*\n\nWhat made me stop second-guessing the design is that we weren't alone. Stripe put its whole API behind `stripe_api_search`\n\n. Sentry exposes 9 of 46 through `search_sentry_tools`\n\n. GitHub removed its dynamic toolset tools and looks to be building a search replacement. Three companies with no reason to coordinate landed on search-then-execute in the same window.\n\n```\nclaude mcp add --transport http appwrite https://mcp.appwrite.io/\n```\n\nNo API key, no project ID, no config editing to switch projects. Projects and organizations are parameters on the call now instead of properties of the credential.\n\nstdio didn't go away. I removed it in the hosted refactor and put it back two days later, because self-hosted users need it. It runs on a project API key and gets 647 of the 981 methods, since a project key can't reach console-level operations anyway.\n\nBehind that URL there's also OpenTelemetry, Sentry, Grafana dashboards, and region routing so a project in another Cloud region doesn't return `general_access_forbidden`\n\n. You end up operating a service, not publishing a package. That's the part I underestimated most.\n\nThe transport is not where the time goes. The authorization spec and everything it pulls in is where the months disappear.\n\nTest against real clients early and expect them to disagree. A logging proxy in front of your server was worth more to me than another pass through the docs.\n\nAssume the spec moves under you. Between starting and shipping, sessions were removed, RFC 7591 was deprecated, and a stateless revision landed. Anthropic donated MCP to the Agentic AI Foundation in December 2025, so it isn't even one vendor's project anymore.\n\nAnd don't hand your API surface over as your tool surface. The architecture this server has today came out of a bug report from a user who was annoyed with us, which I think is the correct way for this to have gone.\n\nThe server is open source at [github.com/appwrite/mcp](https://github.com/appwrite/mcp), and the hosted one is at `https://mcp.appwrite.io/`\n\n.", "url": "https://wpnews.pro/news/how-i-built-the-appwrite-mcp-server-and-decided-to-hide-most-of-its-capabilities", "canonical_source": "https://dev.to/chiragagg5k/how-i-built-the-appwrite-mcp-server-and-decided-to-hide-most-of-its-capabilities-4jm8", "published_at": "2026-08-04 03:37:36+00:00", "updated_at": "2026-08-04 03:51:57.865654+00:00", "lang": "en", "topics": ["developer-tools", "ai-tools", "ai-infrastructure"], "entities": ["Appwrite", "Anthropic", "Christy", "Matej", "Model Context Protocol", "RFC 9728", "RFC 8707", "RFC 7591"], "alternates": {"html": "https://wpnews.pro/news/how-i-built-the-appwrite-mcp-server-and-decided-to-hide-most-of-its-capabilities", "markdown": "https://wpnews.pro/news/how-i-built-the-appwrite-mcp-server-and-decided-to-hide-most-of-its-capabilities.md", "text": "https://wpnews.pro/news/how-i-built-the-appwrite-mcp-server-and-decided-to-hide-most-of-its-capabilities.txt", "jsonld": "https://wpnews.pro/news/how-i-built-the-appwrite-mcp-server-and-decided-to-hide-most-of-its-capabilities.jsonld"}}