文章复盘 Appwrite MCP 从本地 stdio 服务走向托管版本时遇到的认证、项目作用域和传输协议演进问题,并解释为何主动隐藏多数能力。核心启示是 MCP 工程难点不在 JSON-RPC,而在凭证边界、权限模型与安全暴露面。
当 Anthropic 在 2024 年 11 月 25 日推出 Model Context Protocol 时,所有人的目光都被它吸引了,包括当时担任 Appwrite Engineering Lead 的 Christy。那时我刚刚开始做「Engineering Intern」,完全不知道一种全新的协议意味着什么,也不明白为什么大家如此重视它。
只看表面的话,我的理解也不算完全错误。MCP 就是 JSON-RPC,再附加一套 schema 和一次握手。真正花掉我们十六个月的,是围绕它附加的所有东西。
MCP 刚发布时,Streamable HTTP 还不存在。直到 2025-03-26 版本,它才取代 HTTP+SSE。
到 2025 年 2 月 26 日,Christy 已经在仓库里实现了一个可用的 stdio server。我们已经有 API key,所以接线很简单:
claude mcp add appwrite \
--env APPWRITE_PROJECT_ID=<YOUR_PROJECT_ID> \
--env APPWRITE_API_KEY=<YOUR_API_KEY> \
--env APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1 \
-- uvx mcp-server-appwrite
按照设计,一个 API key 的作用域严格限定在单个项目内,因此能力上限从凭证层面就已经被写死了。切换项目意味着要修改编辑器配置。创建项目是不可能的,任何组织级别的操作同样不可能。
两种传输方式之间的全部差异,就在于凭证。托管版本所有棘手的问题,都源于这样一个变化:把项目所属的凭证,替换成用户所属的 token。
按照规范,授权确实是可选的:
对 MCP 实现而言,Authorization 是可选的。……使用基于 HTTP 传输方式的实现,应该遵循本规范。
但对于一个只需调用一次 tool 就能删除数据库的服务,我们无法放心地把授权当成可选项。如果你使用 Auth0 或 WorkOS,这件事无非就是一个配置页面。Appwrite 的一切都坚持自主实现,因此 Matej 构建了 authorization server,我则构建了 resource server,并补齐了 Cloud 中仍然缺失的部分,直到真实客户端能够正常工作。

第 2 步到第 6 步,正是让「只需粘贴这个 URL」成为可能的部分。没有任何东西是预先配置好的。
整个流程由三个 RFC 支撑。Protected Resource Metadata(RFC 9728)是整套授权规范中唯一真正的 MUST:
{
"resource": "https://mcp.appwrite.io/",
"authorization_servers": ["https://cloud.appwrite.io/v1/oauth2/console"],
"scopes_supported": ["..."],
"bearer_methods_supported": ["header"]
}
Resource Indicators(RFC 8707)会把我们的规范 URI 写入 token 的 aud,这样,为其他服务签发的 token 就无法被拿来对我们进行重放攻击。Dynamic Client Registration(RFC 7591)则允许客户端自行注册。再加上使用 S256 的 PKCE 和 RFC 8414 discovery,整个方案就基本成形了。
RFC 本身有文档。文档没有告诉你的是:每个客户端对这些规范的理解都不一样,而你往往要到生产环境里才会发现:
Raycast 使用的是 2025-03-26 版授权规范,它查找的是 /.well-known/oauth-authorization-server,而不是 protected-resource 路由。我是在 server 前面放了一个日志代理,观察它实际请求了什么之后,才发现这一点。
Claude Code 每次运行都会重新进行身份验证。它监听的是一个临时的 loopback 端口,因此 redirect URI 永远无法匹配。OAuth 的原生应用 BCP(RFC 8252 §7.3)规定,必须允许 127.0.0.1 上的任意端口。我们当时没有这么做。
我们自己的 scope 目录也破坏了整个流程。大约 118 个细粒度 scope 生成了一个约 2,680 个字符的 scope 参数,但 validator 的上限是 2,048 个字符。结果是,根本没有人能够到达 consent screen。
如果你正准备构建类似系统,这里有一个提醒:在 2025-11-25 版本中,RFC 7591 从 SHOULD 降级为 MAY;到 2026-07-28,它已经被弃用,替代方案是 Client ID Metadata Documents。我们也发布了对它的支持。规范的这一部分仍在变化。
2025-06-18 版规范允许 server 在返回 InitializeResult 时一并下发 Mcp-Session-Id,并支持通过 DELETE 终止会话、通过 Last-Event-ID 恢复连接。我们把这些全部跳过了:
StreamableHTTPSessionManager(app=server, json_response=False, stateless=True)
每个请求都会携带 bearer token。验证 token,用它构建 client,然后处理调用。没有需要存储的状态,重启时不会丢失任何东西,也不需要让请求固定在某个 replica 上。
事实证明,这是一个正确的选择——尽管我完全不能把功劳揽到自己身上。2026-07-28 版规范彻底移除了协议中的 session。Mcp-Session-Id、initialize 握手、GET SSE stream:全都没了。我们仍然保留的是版本协商,因为你无法替客户端决定它使用哪个协议版本。
像我这样的许多知识分子肯定都想过:MCP 到底为什么存在?难道不能用——比如说——REST,把这件事简化 100 倍吗?
模型只知道训练数据,以及你在运行时交给它的内容。如果它从未见过 Appwrite,就绝不可能猜出下面这些东西:
POST https://<REGION>.cloud.appwrite.io/v1/tablesdb
X-Appwrite-Project: <PROJECT_ID>
X-Appwrite-Key: <API_KEY>
Content-Type: application/json
{ "databaseId": "unique()", "name": "Production" }
endpoint、header 名称,以及 unique() 是一个魔法值这件事,它都不可能知道。而通过 MCP,同一个操作会以自描述的形式出现:
{
"name": "tables_db_create",
"description": "Create a database in an Appwrite project",
"inputSchema": {
"type": "object",
"properties": {
"databaseId": { "type": "string" },
"name": { "type": "string" }
},
"required": ["databaseId", "name"]
}
}
你大可欣慰:AI 需要的手把手指导,比你多得多——至少目前如此。
我研究过的每个 MCP server,都只提供了一组经过精心挑选的小规模 tool。Appwrite 则为每个 SDK method 生成一个 tool,最终覆盖 38 个 service、总计 981 个 method。

数据统计截至 2026 年 8 月。GitHub 的 90 个 tool 被分成 22 个 toolset,其中默认启用 5 个。
「把它们全部暴露出来」这条路无论如何都走不通,背后有两个互不相关的原因。
首先,客户端接不住这么多 tool。2025 年初,Cursor 的文档明确写着,它「只会把前 40 个 tool 发送给 Agent」,而且会静默截断。Windsurf 则会在超过 50 个时直接拒绝。

Discord,liviu74,2025 年 3 月 14 日。Windsurf 直接拒绝;Cursor 虽然接受了,但会静默丢弃第 41 个及之后的 tool。
更让人难受的是,当时我们已经提供了按 service 筛选的 flag。一位社区用户创建了 issue #17:「请减少 tool 数量」:
Cursor 可使用的 MCP tool 上限是 40 个,但仅 Appwrite 自己就有 195 个 tool,因此它既无法与其他 tool 一起使用,甚至连 Appwrite 自己的全部 tool 都用不了。
我指出,可以使用 --databases 缩小范围。对方回复:
这太不合理了。难道我每次要做不同的工作,都必须修改 MCP 参数设置吗……?而且,仅
--databases就有 42 个 tool,已经超过了 Cursor 建议的上限(40 个)。
在远未达到这些硬上限之前,质量就已经开始下降了。来自不同方向的数据最终指向了相近的结论。Anthropic 认为,「一旦可用 tool 超过 30~50 个」,性能就会下降。OpenAI 建议「在一个 turn 开始时,function 数量少于 20 个」。Block 的 Goose 建议控制在 50 个以内。
而且,解决方案的效果可以明确测量。Anthropic 的 advanced tool use 工作在启用 search tool 后,让 Opus 4 在 MCP tool-use eval 中的成绩从 49% 提升到 74%,让 Opus 4.5 从 79.5% 提升到 88.1%,同时减少了 85% 的 definition token。RAG-MCP 则让 tool 选择准确率提升到原来的三倍以上:43.13%,对比 13.62%。
但这里也有一个不利于我观点的注意事项:MCPVerse 发现,一些 agentic model 完全能够处理庞大的 action space。Claude-4-Sonnet 使用 oracle tool set 时得分为 62.3,使用约 220 个 tool 时得分为 62.4。庞大的 tool 目录并非致命问题。但当 Agent 在 981 个 tool 中只需要三个时,这就是一笔毫无意义的额外开销。
appwrite_get_context 会回答你当前所处的位置,以及你能看到哪些项目。
appwrite_search_tools 使用自然语言搜索隐藏的 tool 目录。
appwrite_call_tool 按名称调用其中一个 tool。
appwrite_search_docs 对 Appwrite 文档进行语义搜索。
搜索会在请求发生时缩小范围,因此可以彻底删除按 service 筛选的 flag。修改操作要求传入 confirm_write: true,而对于大到无法放进对话的结果,则会改为 MCP resource。
appwrite_search_tools 背后的评分机制刻意设计得很笨:针对 tool name、description、service 和 resource 进行 token 与 substring 匹配;当查询中推断出的动词与 tool 的动词一致时加分,不一致时扣分。不使用 embedding,不需要重建 index,hot path 中也没有 inference call。
下面是客户端看到的内容:

981 个 method,隐藏在 4 个 tool 和 1 个 resource 背后。「Logout」链接对应的是 OAuth session。
真正让我不再反复怀疑这个设计的原因,是我们并非孤例。Stripe 把整个 API 都放在 stripe_api_search 后面。Sentry 通过 search_sentry_tools 暴露 46 个 tool 中的 9 个。GitHub 移除了 dynamic toolset tool,看起来也在构建一个以搜索为基础的替代方案。三家公司没有任何理由互相协调,却在同一时期不约而同地采用了「先搜索、再执行」的模式。
claude mcp add --transport http appwrite https://mcp.appwrite.io/
不需要 API key,不需要 project ID,切换项目也不需要修改配置。现在,项目和组织是调用时传入的参数,不再是凭证的属性。
stdio 并没有消失。我在托管版重构时移除了它,两天后又把它加了回来,因为 self-hosted 用户需要它。它依靠项目 API key 运行,可以使用 981 个 method 中的 647 个,因为项目 key 本来就无法访问 console-level 操作。
在这个 URL 背后,还有 OpenTelemetry、Sentry、Grafana dashboard,以及 region routing;这样,当项目位于另一个 Cloud region 时,就不会返回 general_access_forbidden。最终,你运营的是一项服务,而不只是发布一个 package。这是我最严重低估的部分。
真正耗费时间的不是传输层。让数月时间消失不见的,是授权规范,以及它连带引入的所有东西。
尽早用真实客户端测试,并做好它们彼此理解不一致的准备。对我而言,在 server 前面放一个日志代理,比再通读一遍文档更有价值。
要假定规范会在你脚下不断变化。从我们开始开发到正式发布,session 被移除了,RFC 7591 被弃用,还出现了一个 stateless 版本。2025 年 12 月,Anthropic 将 MCP 捐赠给了 Agentic AI Foundation,因此它甚至已经不再是某一家厂商的项目。
还有,不要直接把 API surface 当成 tool surface 交出去。这台 server 如今采用的架构,源自一位对我们感到不满的用户提交的 bug report。我觉得,这正是它本该形成的方式。
server 的开源代码位于 github.com/appwrite/mcp,托管版本位于 https://mcp.appwrite.io/。
如需采取进一步措施,你可以考虑屏蔽此人和/或举报滥用行为。