把传统 SaaS 服务接入 MCP:一份可以照着做的实操教程

如果你手上有一个传统 SaaS 服务(有一套 REST/RPC API、有一套只给自家前端用的登录态),现在想让它能被 Claude、Cursor 这类 MCP 客户端直接调用,这篇教程按五个步骤把整个改造过程讲清楚:

  1. 动手前先搞清楚的背景概念
  2. 接入前的确认清单
  3. 用标准 OAuth(Authorization Code + PKCE)改造已有登录系统
  4. 实现 MCP Server 本体
  5. 让 MCP Server 调用已有的 SaaS 服务(关键:token 怎么换)
  6. 在 MCP 之上写 Skill

跟着顺序做下来,你会得到一个完整的、可以被任何 MCP 客户端发现和调用的服务。

0. 背景:MCP 化之后,系统会长成什么样

先建立一个全局印象。改造完成后,系统通常会分成三层:

1
2
3
4
5
授权服务器 (AS)              资源服务器 (RS)                Skill 编排层
────────────── ────────────── ──────────────
签发 token 的地方 → MCP 协议入口: → 把细粒度工具组合
(可以是你已有的登录 验证 token、暴露工具、 成 Agent 能可靠执行
系统,也可以新建一个) 调用已有 SaaS 能力 的完整业务流程
  • 授权服务器(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 第三方接入全都是这个模式。

接入做法

  1. 注册客户端:MCP 客户端需要在你的授权服务器登记,拿到 client_id(如果面向公开的第三方客户端,建议支持 RFC 7591 动态客户端注册 DCR,让客户端自助注册,不需要人工审核每一个)。
  2. 实现标准四个端点
端点 作用
GET /authorize 用户登录 + 同意授权的页面入口
POST /oauth/tokengrant_type=authorization_code 用 code 换 token
POST /oauth/tokengrant_type=refresh_token 续期
POST /oauth/revoke 用户撤销授权(RFC 7009)
  1. 授权页面:用户打开 /authorize 后,走你现有的登录系统完成身份验证(复用已有的账号密码/SSO/2FA,不用重新做一套登录),然后展示”这个应用申请以下权限”,让用户勾选同意的 scope。
  2. 签发 token:同意后,重定向回调地址带上 code;调用方拿 code + PKCE 的 code_verifieraccess_token + refresh_token
  3. refresh 续期:access_token 过期后用 refresh_token 续期,开启 refresh token rotation(每次续期发新的 refresh_token,旧的失效)是安全最佳实践。
  4. 撤销:在你现有的用户设置里加一个”已授权的第三方应用”列表,用户可以随时点撤销,撤销后对应的 refresh_token 立即失效。

JWT claims 建议:如果 token 是 JWT,sub 用真实用户的唯一标识,scope 记录本次授权的权限范围,方便资源服务器做鉴权、方便审计。

1
2
3
4
{
"sub": "<user_id>",
"scope": "read:messages send:messages"
}

如果你的 SaaS 原来的登录方式很”非标准”(比如自定义 App 唤起、扫码登录、短信验证码),不用担心——这些都可以保留在 /authorize 页面内部的登录环节里,标准 OAuth 只规定”外层协议长什么样”,你原来的登录体验完全可以原样保留,只是最终要产出一个标准的 code

3. 实现 MCP Server 本体

认证方案定了之后,开始写 MCP Server。这一步的核心原则是:MCP Server 只做协议层的事,业务逻辑尽量薄,实际调用转发给已有 SaaS API

3.1 最小骨架

用官方 MCP SDK(TypeScript/Python 均可)起一个标准骨架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

const server = new McpServer({ name: "your-saas-mcp", version: "1.0.0" });

server.registerTool(
"list_orders",
{
description: "列出当前用户的订单",
inputSchema: { status: z.string().optional() },
},
async ({ status }, ctx) => {
const userToken = ctx.auth.accessToken; // 见第 4 节
const orders = await callYourSaasApi("/orders", { status, token: userToken });
return { content: [{ type: "text", text: JSON.stringify(orders) }] };
}
);

建议约定:一个工具一个文件。工具多了之后,统一放进一个目录(比如 tools/list-orders.tstools/send-message.ts),每个文件只关心自己这一个工具的输入校验、鉴权检查、调用转发、错误处理。

3.2 必须暴露的两个协议端点

MCP Server 作为资源服务器需要实现:

(1)JWKS 验签,不要自己维护登录态

1
2
3
4
5
6
7
8
9
10
11
import { createRemoteJWKSet, jwtVerify } from "jose";

const JWKS = createRemoteJWKSet(new URL(`${AUTH_ISSUER}/.well-known/jwks.json`));

async function verifyToken(token: string) {
const { payload } = await jwtVerify(token, JWKS, {
issuer: AUTH_ISSUER,
audience: YOUR_RESOURCE_ID, // 必须校验,防止别的服务的 token 被拿来这里用
});
return payload;
}

(2)受保护资源元数据(RFC 9728),让客户端自动发现该去哪里认证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
app.get("/.well-known/oauth-protected-resource/mcp", (req, res) => {
res.json({
resource: `${PUBLIC_BASE_URL}/mcp`,
authorization_servers: [AUTH_ISSUER],
});
});

// 未带 token 的请求:401 + 指路
app.use("/mcp", (req, res, next) => {
if (!req.headers.authorization) {
res.status(401)
.set("WWW-Authenticate", `Bearer resource_metadata="${PUBLIC_BASE_URL}/.well-known/oauth-protected-resource/mcp"`)
.end();
return;
}
next();
});

注意:不要在 MCP Server 里再实现一遍 /authorize/register 这些属于授权服务器的路径。如果客户端不小心请求到这里,直接返回 404,逼它走标准的发现流程去找授权服务器。

3.3 每个工具的鉴权:声明式 scope

给每个工具的元数据加一个 requiredScope,用一个统一的中间件做校验,不要在每个工具内部各写一遍:

1
2
3
4
5
6
function resolveToolAuth(payload: JWTPayload, requiredScope: string) {
const granted = (payload.scope as string ?? "").split(" ");
if (!granted.includes(requiredScope)) {
throw new Error(`insufficient_scope: need ${requiredScope}`);
}
}

工具的 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
2
3
4
5
6
7
// 签发 MCP access_token 时
const legacySession = await loginToLegacySaas(userCredentials); // 已有系统的登录
await redis.set(`legacy-session:${sub}`, encrypt(legacySession), { EX: ttl });

// MCP 工具调用时
const legacySession = decrypt(await redis.get(`legacy-session:${payload.sub}`));
await callLegacyApi(endpoint, { session: legacySession });

适用场景:MCP Server 和授权服务器是同一套团队维护,已有系统的登录态可以被合法地缓存/转发。

方案二:Token Exchange 换取上游凭据(标准化做法)

如果 MCP 的授权服务器和已有 SaaS 的身份源是分离的(比如授权服务器是新建的,已有系统的登录还是老样子),用 RFC 8693 Token Exchange:MCP Server 拿着用户的 MCP access_token,找授权服务器换一份”上游凭据包”。

1
2
3
4
5
curl -X POST {AS}/token \
-H "Authorization: Basic $(base64 mcp_server_client_id:mcp_server_secret)" \
-d grant_type=urn:ietf:params:oauth:grant-type:token-exchange \
-d subject_token=<用户的 MCP access_token> \
-d resource=<这个 MCP Server 自己的资源标识>

授权服务器内部逻辑:校验 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
---
name: order-management
description: >
处理订单相关任务时使用本 skill。触发场景包括:"帮我查一下最近的订单"
"取消订单 XXX""订单 XXX 什么时候发货"等。
---

# 订单管理 Skill

## 可用工具
| 工具 | 用途 |
|---|---|
| list_orders | 列出订单,支持按状态过滤 |
| get_order | 获取单个订单详情 |
| cancel_order | 取消订单(有副作用,需要用户明确确认) |

## 核心流程

### 查询订单
1. 若用户提到时间范围("上周""这个月"),先调用 get_local_time 拿到用户真实时区,再计算边界日期,禁止假设 UTC。
2. 调用 list_orders,若结果为空,明确告知用户"未查到匹配订单",不要编造。

### 取消订单(有副作用操作)
1. 取消前必须先用 get_order 展示订单详情,让用户二次确认。
2. 确认后才能调用 cancel_order。
3. 调用失败时如实反馈失败原因,不要假装成功。

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 客户端连上就用的服务。