生产级 MCP 服务如何开放 SaaS 数据
Howseen 分享将 SaaS 数据接入 Claude、ChatGPT 等客户端的 MCP 服务实现,包含 26 个读取工具和 18 个写入工具。文章以真实代码说明接口设计,并展示无需密钥发现工具及未认证调用的检查方式。
AI 深度解读
SaaS 的数据留在仪表盘里,用户却已经在 Claude、ChatGPT、Cursor 中工作。Howseen 的做法是给产品加一个 MCP 服务,让助手直接查询品牌在 AI 回答中的表现,并执行部分操作。作者介绍的生产版本是 1.3.0,共有 44 个工具,其中 26 个只读、18 个可写。
这篇最有用的部分,是如何把鉴权、数据一致性和写操作约束放进服务端。它提供了一套可参考的实现,但「模型传了确认参数」是否等于「用户真的同意」,还需要单独审视。
为什么无密钥能看工具,却不能查数据
Howseen 监测 ChatGPT、Gemini、Perplexity 和 Google 的 AI 回答如何谈论品牌。接入 MCP 后,用户可以直接问助手:哪些问题提到了竞品却没提到自己,哪些引用来源值得争取,上次扫描后发生了什么变化。
接入的第一关是工具发现。作者指出,目录和客户端通常先调用 initialize,再调用 tools/list。如果这时就返回 401,尚未配置密钥的服务容易被看成不可用。
因此,他们允许匿名读取静态元数据,但查询业务数据必须带密钥。开放的方法包括 initialize、notifications/initialized、ping、tools/list、prompts/list、resources/list、resources/templates/list。
这里有两个容易漏掉的边界。JSON-RPC 支持批量请求,整个批次必须全部属于发现方法;只要混入一个 tools/call,整批就需要鉴权。匿名上下文中的数据库对象还被替换成了 Proxy,任何属性访问都会抛错,避免工具代码误查数据。
个人密钥以 hs_live_ 开头,只展示一次,数据库仅保存其 sha256 哈希。正常通过 Authorization: Bearer hs_live_... 传递;对无法发送自定义请求头的应用,也支持 URL 参数 ?key=。作者明确要求把这种 URL 当密码保管,泄露后可在 Settings 撤销。
缺少或使用无效密钥时,服务返回 HTTP 401 和 JSON-RPC 错误体。原文的无密钥示例还包含 WWW-Authenticate: Bearer realm="howseen",错误码是 -32001。错误消息会告诉用户去哪里创建密钥,因为看到它的人往往正在聊天窗口里,而非查看服务日志。
Next.js 接 MCP,先处理请求生命周期
服务部署在 Next.js route handler 和 serverless 运行环境中。作者选择 Streamable HTTP,但关闭会话:每次 POST 都创建一个 server 和 transport,不依赖进程内存保存上次请求的状态。
下面这段代码解决的是请求之间不保留会话的问题:
const server = buildMcpServer(ctx);
const transport = new WebStandardStreamableHTTPServerTransport({
sessionIdGenerator: undefined,
enableJsonResponse: true,
});
await server.connect(transport);
return withCors(await transport.handleRequest(fwd));
重点看 sessionIdGenerator: undefined 和 enableJsonResponse: true:前者关闭会话,后者返回普通 JSON,不开启事件流。
因此,要求 text/event-stream 的 GET 会收到 405;普通 GET 则返回名称、版本、工具名和鉴权规则,也充当健康检查。
还有个很适合 Node 开发者记下的细节:服务为了判断鉴权规则,已经读取过一次请求体。交给 transport 时,必须用相同字节重新构造一个 Request,否则下游拿到的是已经消费过的 body。
查询结果的一致性同样重要。26 个只读工具复用 REST API 的数据层,让聊天窗口与仪表盘使用同一套计算逻辑。比如 get_prompt_gaps 查竞品覆盖缺口,get_easiest_wins 查引用来源,get_changes 查扫描变化,search_responses 返回回答片段和引用。
每个工具还声明 readOnlyHint、destructiveHint、openWorldHint。例如 list_brands 的三个值依次是 true、false、false,帮助客户端判断调用方式。这些是工具行为提示,真正的数据权限仍由服务端控制。
写操作默认预览,确认流程仍有边界
18 个写工具覆盖跟踪问题、添加竞品、起草文章、发布文章和请求索引等操作。它们统一经过同一个门禁,依次检查写权限、业务限制和确认参数:
export function writeGate(o: { scopes: readonly string[] | null | undefined; confirm?: boolean | null; blocked?: string | null }): GateResult {
if (!hasScope(o.scopes, "write")) return { kind: "forbidden", message: READ_ONLY_KEY_MESSAGE };
if (o.blocked) return { kind: "blocked", message: o.blocked };
if (o.confirm !== true) return { kind: "preview" };
return { kind: "execute" };
}
要看的是第三个判断:没有明确传入 confirm: true,工具就返回预览,不改变数据。
密钥默认只读,用户需要在 Settings 选择「Read + actions」,获得 write scope。套餐限制和配额也与应用内一致,超限会被拒绝。预览会说明改动内容、配额影响,以及再次提交时需要携带的准确参数;执行后记录日志。
服务指令进一步要求模型展示预览、等待用户明确答应,而且不能用一次批准连续执行多个变更。
这段门禁能保证缺少确认参数时不执行,但不能独立证明用户已经确认。
模型仍可能直接传入 confirm: true。原文没有展示预览令牌、参数绑定或独立批准记录,因此不能把这套实现理解成服务端已经验证了人的授权。
此外,服务提供 weekly_review、client_report、fix_my_site 三个预设 prompt,用来串联常见任务。每个密钥限流为每分钟 120 次、每天 5,000 次。
它也以 ai.howseen/howseen-mcp 登记到官方 MCP registry,remote 类型为 streamable-http,并将 Authorization 标记为 isRequired: true、isSecret: true。开放发现让目录无需获取用户凭据,就能读取工具清单。
我的判断
我赞同优先复用数据层、匿名上下文禁止数据库访问、写操作默认预览。这几项直接对应 SaaS 接入助手时最容易出现的问题:两处数字不同、鉴权遗漏,以及自然语言请求意外落成真实变更。
但文章提供的是生产实现经验,没有给出接入成功率、延迟、成本或不同架构的对照数据。44 个工具说明覆盖范围,不足以证明工具越多越好。
无状态方案符合他们当前的 serverless 部署条件。是否适合其他团队,还要看任务是否需要跨请求保留状态、持续传输结果;原文没有展开这些需求。类似地,匿名发现建立在「元数据可以公开」的前提上,不能直接套到连工具名称都属于内部信息的系统。
相比只在 prompt 里要求谨慎,代码门禁更可靠;相比把人工批准与具体操作绑定,它仍有缺口。发布内容这类操作若需要严格确认,应把「模型声明已确认」和「系统验证过批准」分开设计。
能拿来做什么
- 如果你的 Next.js 或 Node 产品已经有 REST API,先让 MCP 工具复用现有数据层,并核对相同筛选条件下的结果是否一致,再逐步增加工具。
- 在鉴权检查中覆盖匿名
tools/list、匿名tools/call和混合批次请求,同时确认匿名上下文无法访问数据库。 - 给写工具统一添加权限检查、配额检查、默认预览和执行日志;对发布等重要操作,进一步把批准与本次参数绑定。
- 把 MCP 错误消息当成用户界面来写,明确说明缺少什么权限、触发什么限制,以及用户下一步该做什么。