如果你手上有一个传统 SaaS 服务(有一套 REST/RPC API、有一套只给自家前端用的登录态),现在想让它能被 Claude、Cursor 这类 MCP 客户端直接调用,这篇教程按五个步骤把整个改造过程讲清楚:
- 动手前先搞清楚的背景概念
- 接入前的确认清单
- 用标准 OAuth(Authorization Code + PKCE)改造已有登录系统
- 实现 MCP Server 本体
- 让 MCP Server 调用已有的 SaaS 服务(关键:token 怎么换)
- 在 MCP 之上写 Skill
跟着顺序做下来,你会得到一个完整的、可以被任何 MCP 客户端发现和调用的服务。
0. 背景:MCP 化之后,系统会长成什么样
先建立一个全局印象。改造完成后,系统通常会分成三层:
1 | 授权服务器 (AS) 资源服务器 (RS) Skill 编排层 |
- 授权服务器(AS):负责登录、颁发 token。如果你的 SaaS 已经有一套成熟的登录系统(哪怕不是标准 OAuth),这一层的工作是把它”包装”成标准 OAuth 端点,而不是推倒重来。
- 资源服务器(RS):这是你新写的 MCP Server 本体,只做”验证请求身份 + 把工具调用转发给已有 SaaS API”这两件事,本身不持有用户密码、不管登录 UI。
- Skill 层:MCP 只解决”Agent 能不能调用某个具体接口”,不解决”Agent 能不能可靠地完成一个完整任务”。这层是给 Agent 用的”操作手册”,第 5 节详细讲。
明确了这个全局结构,下面开始一步步做。
1. 接入前的确认清单
动手写代码之前,先把下面这份清单过一遍,能省掉后面返工的大部分时间。
1.1 认证相关
- 你的 SaaS 现有登录方式是什么?(账号密码 + session?企业 SSO?扫码/短信验证码?)
- 用户能不能对”第三方应用访问自己数据”这件事做出授权决策?有没有现成的”已授权应用列表 / 撤销”页面?
- token 的生命周期怎么管理?现有 session 是短期的还是长期免登录的?
1.2 能力盘点
- 列出你打算开放给 Agent 的能力清单(读联系人、发消息、查订单、下载文件……),标注每个能力是”只读”还是”有副作用”(发送、删除、支付类操作需要格外小心)。
- 每个能力现有的接口是什么形式(REST?RPC?SDK?内部 CLI?),返回的数据结构是否需要清洗后才适合喂给 LLM。
- 哪些能力必须限定在特定角色/权限下才能使用?
- 是否需要区分测试环境和生产环境的凭据?
1.3 基础设施
- 有没有 Redis / 数据库这类可以做 token、会话状态持久化的基础设施(多节点部署时尤其需要,不能只存内存)?
- 有没有对外可访问的公网域名(MCP 客户端需要能访问到你的
/mcp端点)?
把这份清单填完,你会清楚接下来第 2、4 步该怎么落地——尤其是已有登录系统要怎么包装、token 换取要用哪种方案。
2. 用户代理式认证:Authorization Code + PKCE
MCP 官方推荐的鉴权方式是标准 OAuth 2.0(客户端发现 → 授权 → 拿 token → 调用)。绝大多数场景下,Agent 是”代表某个真实用户”去操作——以这个人的身份发消息、读他的数据,这也是安全要求最高、最需要认真设计的一种,本节详细展开。
标准依据:RFC 6749 §4.1(Authorization Code)+ RFC 7636(PKCE)。业界同款:Slack Apps、GitHub Apps、Notion、Google Workspace 第三方接入全都是这个模式。
接入做法:
- 注册客户端:MCP 客户端需要在你的授权服务器登记,拿到
client_id(如果面向公开的第三方客户端,建议支持 RFC 7591 动态客户端注册 DCR,让客户端自助注册,不需要人工审核每一个)。 - 实现标准四个端点:
| 端点 | 作用 |
|---|---|
GET /authorize |
用户登录 + 同意授权的页面入口 |
POST /oauth/token(grant_type=authorization_code) |
用 code 换 token |
POST /oauth/token(grant_type=refresh_token) |
续期 |
POST /oauth/revoke |
用户撤销授权(RFC 7009) |
- 授权页面:用户打开
/authorize后,走你现有的登录系统完成身份验证(复用已有的账号密码/SSO/2FA,不用重新做一套登录),然后展示”这个应用申请以下权限”,让用户勾选同意的 scope。 - 签发 token:同意后,重定向回调地址带上
code;调用方拿code+ PKCE 的code_verifier换access_token+refresh_token。 - refresh 续期:access_token 过期后用 refresh_token 续期,开启 refresh token rotation(每次续期发新的 refresh_token,旧的失效)是安全最佳实践。
- 撤销:在你现有的用户设置里加一个”已授权的第三方应用”列表,用户可以随时点撤销,撤销后对应的 refresh_token 立即失效。
JWT claims 建议:如果 token 是 JWT,sub 用真实用户的唯一标识,scope 记录本次授权的权限范围,方便资源服务器做鉴权、方便审计。
1 | { |
如果你的 SaaS 原来的登录方式很”非标准”(比如自定义 App 唤起、扫码登录、短信验证码),不用担心——这些都可以保留在 /authorize 页面内部的登录环节里,标准 OAuth 只规定”外层协议长什么样”,你原来的登录体验完全可以原样保留,只是最终要产出一个标准的 code。
3. 实现 MCP Server 本体
认证方案定了之后,开始写 MCP Server。这一步的核心原则是:MCP Server 只做协议层的事,业务逻辑尽量薄,实际调用转发给已有 SaaS API。
3.1 最小骨架
用官方 MCP SDK(TypeScript/Python 均可)起一个标准骨架:
1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; |
建议约定:一个工具一个文件。工具多了之后,统一放进一个目录(比如 tools/list-orders.ts、tools/send-message.ts),每个文件只关心自己这一个工具的输入校验、鉴权检查、调用转发、错误处理。
3.2 必须暴露的两个协议端点
MCP Server 作为资源服务器需要实现:
(1)JWKS 验签,不要自己维护登录态
1 | import { createRemoteJWKSet, jwtVerify } from "jose"; |
(2)受保护资源元数据(RFC 9728),让客户端自动发现该去哪里认证
1 | app.get("/.well-known/oauth-protected-resource/mcp", (req, res) => { |
注意:不要在 MCP Server 里再实现一遍 /authorize、/register 这些属于授权服务器的路径。如果客户端不小心请求到这里,直接返回 404,逼它走标准的发现流程去找授权服务器。
3.3 每个工具的鉴权:声明式 scope
给每个工具的元数据加一个 requiredScope,用一个统一的中间件做校验,不要在每个工具内部各写一遍:
1 | function resolveToolAuth(payload: JWTPayload, requiredScope: string) { |
工具的 scope 划分建议贴近用户能理解的权限边界(”读订单” “发消息” “下载附件” 这种颗粒度),不要做成一个大而化之的 * 权限——用户在授权页面勾选时才有意义。
4. 让 MCP Server 调用已有的 SaaS 服务:token 怎么换
这是整个改造里最容易卡住的一步:MCP Server 手里拿到的是”MCP 自己签发的 access_token”,但已有的 SaaS API 认的是它自己原来的登录态(session cookie、内部 token、SDK 凭据……)。两者不是一回事,需要一层”换取”逻辑。根据你已有 SaaS 的登录态类型,有三种常见做法。
方案一:MCP token 本身就是对已有系统的包装(最简单)
如果你能接受”MCP 的 access_token 载荷里直接带着调用已有系统所需的信息”,可以让授权服务器在签发 MCP token 的同时,把已有系统的登录凭据(或者能换出登录凭据的引用)一并塞进去,比如加密后放在 token 里,或者存一份到 Redis,token 里存一个引用 key。
1 | // 签发 MCP access_token 时 |
适用场景:MCP Server 和授权服务器是同一套团队维护,已有系统的登录态可以被合法地缓存/转发。
方案二:Token Exchange 换取上游凭据(标准化做法)
如果 MCP 的授权服务器和已有 SaaS 的身份源是分离的(比如授权服务器是新建的,已有系统的登录还是老样子),用 RFC 8693 Token Exchange:MCP Server 拿着用户的 MCP access_token,找授权服务器换一份”上游凭据包”。
1 | curl -X POST {AS}/token \ |
授权服务器内部逻辑:校验 subject_token 合法后,从自己保存的身份映射里取出这个用户在已有系统里的登录凭据,打包返回给 MCP Server。这份凭据包建议做成一次性(取一次即失效),MCP Server 拿到后立刻用来登录已有系统,不要长期持有明文凭据。
适用场景:多个 MCP Server(对应多个不同的 SaaS 能力模块)共享同一个授权服务器时,这个模式扩展性最好——每接入一个新的 MCP Server,只需要在授权服务器的”机密客户端注册表”里加一条,指定它能为哪个 resource 做 exchange,不需要每个 MCP Server 都各自实现一套”怎么登录已有系统”的逻辑。
方案三:已有系统本来就是标准 OAuth(最省事)
如果你的 SaaS 本身已经是一个标准 OAuth 提供方(很多开放平台类产品是这样),直接复用它作为你的授权服务器,MCP Server 只需要验证它签发的 token,不需要额外的换取步骤——这是最理想的情况,跳过方案一/二直接进入第 3 步。
选择建议
| 你的情况 | 推荐方案 |
|---|---|
| 已有系统本身就是标准 OAuth | 方案三,直接复用 |
| 新建授权服务器,已有系统登录态可控 | 方案一 |
| 授权服务器和已有系统身份源分离、未来会有多个 MCP Server | 方案二 |
一个通用的性能建议:不管选哪种方案,换取上游凭据这件事要在拿到用户 token 的第一时间做(比如 MCP 会话的 initialize 阶段),而不是等到第一次真正调用工具时才做——否则用户第一次调用工具会有明显的额外延迟。
5. 在 MCP 之上写 Skill
工具都注册好、认证都打通之后,你会发现单靠”一堆独立工具 + 工具的文字描述”,Agent 处理稍微复杂一点的真实任务时经常不靠谱:漏调用某个必要步骤、记错时间范围、在找不到数据时开始瞎编。
原因是:MCP 工具描述解决的是”这个工具是做什么的”,但真实任务往往需要”按什么顺序调用哪几个工具、遇到分支情况怎么处理、有哪些红线不能碰”——这部分知识应该写成 Skill,而不是指望模型每次临场发挥都能拼对。
5.1 Skill 的基本结构
一个 Skill 通常就是一份带 frontmatter 的 Markdown 文档:
1 | --- |
5.2 什么内容值得写进 Skill,而不是塞进工具描述
- 跨工具的编排逻辑:一个任务需要按顺序调用多个工具、有分支判断,这种”工作流”写在 Skill 里,比塞进单个工具的 description 更清晰。
- 横切的行为纪律:不管做什么任务都要遵守的规则(”不许编造信息””涉及日期必须用代码算真实时区,不能靠模型心算””有副作用的操作必须先确认”)。这类规则建议单独写一个”通用行为准则” Skill,让它对所有相关任务强制生效,而不是在每个工具描述里重复一遍、容易漏。
- 有副作用操作的安全护栏:发送、删除、支付类工具,Skill 里要明确写清楚”必须先展示详情+二次确认才能执行”,不要指望模型自己想到要确认。
- 可审计性增强:比如要求所有引用了外部数据的回答都标注来源,这种”回答质量”层面的要求也适合做成一个独立的 Skill。
5.3 一句话原则
MCP 负责暴露”能做什么”,Skill 负责保证”做得对不对、可不可信”。 只做完 MCP 这一层,大概率会在真实场景里发现 Agent 表现不稳定——这时候该补的不是更多工具,而是这一层的编排和纪律。
小结:一份可以打印出来贴墙上的检查清单
- 完成第 1 步的确认清单,尤其是认证现状和能力盘点
- 授权服务器实现 Authorization Code + PKCE 的标准四个端点
- 授权服务器只做登录 + 签发 token,不要和 MCP Server 混在一起
- MCP Server 只验签、发布元数据、转发调用,不持有登录态
- 每个工具一个文件,声明式写清楚所需 scope
- 确定 token 怎么换取已有 SaaS 的调用凭据(方案一/二/三),eager 换取而不是等首次调用才换
- 有副作用的操作,在 Skill 里写清楚二次确认的要求
- 通用行为准则单独成一个 Skill,不要散落在各个工具描述里
按这份清单走一遍,你就有了一个别人能直接拿 Claude / Cursor 等 MCP 客户端连上就用的服务。