BrainWeb/payload-mcp-oauth
GitHub: BrainWeb/payload-mcp-oauth
为 Payload CMS MCP 插件提供 OAuth 2.1 + PKCE 及动态客户端注册支持,使其能够安全连接到 Claude 桌面端和网页端的认证插件。
Stars: 2 | Forks: 0
# @brainwebuk/payload-plugin-mcp-oauth
为
[`@payloadcms/plugin-mcp`](https://www.npmjs.com/package/@payloadcms/plugin-mcp) 提供
OAuth 2.1 + PKCE + 动态客户端注册支持,
从而使基于 Payload 的 MCP server 可以作为 **Custom Connector 添加到 Claude.ai 中**,
并与现有的 API-key 流程并存。
该插件是**纯粹附加的**:它包装了 MCP endpoint 处理程序,并添加了
OAuth endpoint 和 collections。你现有的 API-key MCP 客户端将继续保持
不变地运行。
- 带有 **PKCE(仅限 S256)** 的 OAuth 2.1 授权码流程
- **动态客户端注册**(RFC 7591)— Claude.ai 自动注册
- 通过 RFC 8414 / RFC 9728 well-known 文档进行发现
- 静态加密的 Token(HMAC-SHA-256);支持刷新和撤销
- **OAuth Clients** 和 **OAuth Tokens** 作为管理 collections 显示在
**MCP** 导航组下(仅限管理员;公共 REST/GraphQL 接口保持关闭)
## 要求
| | Version |
|---|---|
| `payload` | `^3.0.0` |
| `@payloadcms/plugin-mcp` | `^3.0.0` (测试版本 3.85.0) |
| `next` | `^14 \|\| ^15 \|\| ^16` (仅适用于导出的 proxy/middleware) |
| Node | `>= 20` |
## 安装
### 1. 添加 package
```
pnpm add @brainwebuk/payload-plugin-mcp-oauth
# 或者:npm i / yarn add
```
### 2. 注册插件(在 `mcpPlugin` 之后)
在 `payload.config.ts` 中,**紧接着** `mcpPlugin()` 之后注册 `payloadMcpOAuth()`,
并向其传递你提供给 `mcpPlugin()` 的**相同** options 对象。
```
import { mcpPlugin } from '@payloadcms/plugin-mcp'
import type { MCPPluginConfig } from '@payloadcms/plugin-mcp'
import { payloadMcpOAuth } from '@brainwebuk/payload-plugin-mcp-oauth'
import { buildConfig } from 'payload'
// Assign ONCE to a const and reuse the same reference in both calls. ⚠️
const mcpOptions: MCPPluginConfig = {
collections: {
users: { enabled: { find: true, update: true } },
media: { enabled: { find: true, create: true } },
},
}
export default buildConfig({
// ...db, collections, admin, etc.
plugins: [
mcpPlugin(mcpOptions),
payloadMcpOAuth({
issuer: process.env.NEXT_PUBLIC_SERVER_URL || 'http://localhost:3000',
mcpPluginOptions: mcpOptions, // ← the SAME object, not a copy
}),
],
})
```
### 3. 添加 proxy (Next.js 16) / middleware (Next.js 14–15)
OAuth 发现 (`/.well-known/...`) 和裸主机 MCP 连接器需要两个
Payload 插件无法自行注册的主机级 URL 重写。该插件将它们作为现成的请求处理程序提供 —— 请使用你的 Next.js 版本所使用的约定文件(靠近你的 `app/` 目录)将其接入。重新导出该处理程序,但需将 `config` 声明为**本地**字面量。
**Next.js 16+** — Next 将 `middleware` 约定重命名为 `proxy`。请创建
`src/proxy.ts`:
```
export { mcpOAuthMiddleware as proxy } from '@brainwebuk/payload-plugin-mcp-oauth/middleware'
export const config = {
matcher: [
'/',
'/.well-known/oauth-authorization-server',
'/.well-known/oauth-protected-resource',
],
}
```
**Next.js 14–15** — `proxy` 约定尚不存在;创建
`src/middleware.ts` 并使用相同的主体,将其导出为 `middleware`:
```
export { mcpOAuthMiddleware as middleware } from '@brainwebuk/payload-plugin-mcp-oauth/middleware'
export const config = {
matcher: [
'/',
'/.well-known/oauth-authorization-server',
'/.well-known/oauth-protected-resource',
],
}
```
已经有 proxy/middleware 了?改为组合使用它们(以 Next 16 为例;在 14–15 版本中
将文件命名为 `middleware.ts`,函数命名为 `middleware`):
```
import type { NextRequest } from 'next/server'
import { createMcpOAuthMiddleware } from '@brainwebuk/payload-plugin-mcp-oauth/middleware'
const mcpOAuth = createMcpOAuthMiddleware() // accepts { apiRoute, mcpEndpointPath, ... }
export function proxy(request: NextRequest) {
// ...your logic first...
return mcpOAuth(request)
}
export const config = {
matcher: ['/', '/.well-known/oauth-authorization-server', '/.well-known/oauth-protected-resource' /* + yours */],
}
```
### 4. 设置环境变量
```
# 客户端访问的公开 HTTPS URL。用作 OAuth issuer 以及在 discovery metadata 中。
NEXT_PUBLIC_SERVER_URL=https://cms.example.com
# 用于对静态 token 进行哈希处理的 HMAC pepper —— 生产环境中必需(>= 32 个字符)。
# 使用以下命令生成:openssl rand -hex 32
PMOAUTH_TOKEN_PEPPER=<64-hex-chars>
```
在开发环境中,如果未设置 `PMOAUTH_TOKEN_PEPPER`,将使用内置的不安全 pepper(并伴有警告)。在 `NODE_ENV=production` 环境下,如果
它缺失或长度少于 32 个字符,插件将在**启动时报错**。
### 5. 重新生成管理导入映射(如果你的应用使用了的话)
此插件不注册任何自定义管理组件,因此它*不需要*重新生成导入
映射。如果你的应用已经维护了 `src/app/(payload)/admin/importMap.js`,
在安装后重新生成它是无害的,并且能保持整洁:
```
pnpm payload generate:importmap
```
(如果你的应用不使用 `src` 目录,请去掉 `src/` 前缀。)
### 6. 应用 schema 变更
该插件添加了 collections(`oauth-clients`、`oauth-auth-codes`、`oauth-tokens`、
`oauth-csrf-nonces`)。使用你的应用已经使用的任何 schema 工作流 —— **不要
混用它们**:
- **Dev 推送**(在开发环境中 SQLite/Postgres 的默认设置):只需启动应用;新的
表将在下次启动时被推送。**不要**针对推送同步的开发数据库运行 `migrate:create`/`migrate` —— 你会遇到 *"table … already exists"* 错误。
- **Migrations**(生产环境):运行 `pnpm payload migrate:create` 以生成包含
新 collections 的 migration,然后运行 `pnpm payload migrate`。
就这些 —— 启动应用,OAuth endpoint 即上线,并在管理侧边栏的 **MCP** 组下显示 **OAuth Clients**
和 **OAuth Tokens**。
## 从 Claude.ai 连接
1. Settings → **Connectors** → **Add custom connector**。
2. 输入你的服务器 URL(裸主机,例如 `https://cms.example.com`,即可生效 —
middleware 会将其路由到 MCP endpoint)。
3. Claude.ai 发现授权服务器,进行动态注册,并启动
OAuth + PKCE 握手。
4. 你将被引导至 Payload 管理员登录页面 + 同意屏幕;批准以
颁发 token。
验证发现端点是否可访问:
```
curl https://cms.example.com/.well-known/oauth-protected-resource
curl https://cms.example.com/.well-known/oauth-authorization-server
```
## 使 MCP 对 AI agents 可用
连接后,agent 只知道 MCP server *告诉*它的内容。从丰富的 collections(具有嵌套块、条件字段等的页面构建器)生成的工具
体积庞大且不明显,因此 agents 在执行 `create*` 调用时会不断试错。利用 `@payloadcms/plugin-mcp` 暴露的引导渠道来弥合这一差距 —— 所有内容都通过
协议以 **server → agent** 的方式传递,因此它们可以覆盖每个客户端(Claude.ai Web、Desktop、Code 以及
非 Claude 的 MCP 客户端):
- **`serverOptions.instructions`** — `mcpPlugin()` 上的一个“如何使用此服务器”字符串。
- **per-collection `description`** — 告诉 agent 何时/为何使用某个 collection。
- **field `admin.description`** — 流入每个工具的输入 schema,因此
agent 会内联读取字段规则(例如“仅在……时需要”)。
- **`prompts`** — agent 可以调用的预构建引导工作流。
```
mcpPlugin({
serverOptions: {
serverInfo: { name: 'Author Website', version: '1.0.0' },
instructions: `
This server manages an author marketing site (pages, posts, media).
- Publish by setting "_status": "published".
- pages.hero.type is none|lowImpact|mediumImpact|highImpact; high/mediumImpact
REQUIRE hero.media (a Media id) — upload first; prefer lowImpact otherwise.
- pages.layout is an array of blocks: content, cta, mediaBlock, archive, formBlock.
- If a tool schema is large, create a minimal doc first, then add blocks with the update tool.`,
},
collections: {
pages: {
description: 'Landing/marketing pages built from a hero + layout blocks.',
enabled: { find: true, create: true, update: true },
},
},
})
```
### Claude Code 的安装助手(可选)
此 repo 还充当具有 `install` 技能的 Claude Code 插件市场,该技能将
引导 Claude Code 完成插件的配置(config、proxy、env、schema)以及
常见陷阱。在 Claude Code 中:
```
/plugin marketplace add BrainWeb/payload-mcp-oauth
/plugin install payload-mcp-oauth@brainwebuk
```
然后要求 Claude Code "install payload-plugin-mcp-oauth"(或运行
`/payload-mcp-oauth:install`)。这有助于安装插件的**开发者**;
因为 Skills 是客户端的,它对运行时的 connector agent 没有影响。
## 配置
| Option | Type | Default | Description |
|---|---|---|---|
| `issuer` | `string` | — (必填) | 公共基础 URL;OAuth 颁发者 + metadata 基础。 |
| `mcpPluginOptions` | `MCPPluginConfig` | — (必填) | 传递给 `mcpPlugin()` 的**相同**对象。 |
| `userCollection` | `string` | `'users'` | 包含用户账号的 collection。 |
| `disabled` | `boolean` | `false` | 在不卸载的情况下关闭 OAuth:无 endpoint,无 token 接入,`mcpPluginOptions` 不受影响(API-key MCP 继续工作)。Collections 保持注册以维持 schema 一致性。当设置了 `mcpPluginOptions.disabled` 时也会自动检测。 |
| `adminAccess` | `Access` | `userCollection` 中经过身份验证的用户 | 谁可以在管理界面中查看/管理 OAuth collections。见下文。 |
| `accessTokenTtlSeconds` | `number` | `3600` | Access-token 生命周期。 |
| `refreshTokenTtlSeconds` | `number` | `86400` | Refresh-token 生命周期。 |
| `authCodeTtlSeconds` | `number` | `300` | 授权码生命周期。 |
| `rateLimits` | `RateLimitOptions` | `{}` | 针对 endpoint 的速率限制覆盖。 |
### 管理界面与访问
`oauth-clients` 和 `oauth-tokens` 作为 collections 显示在 **MCP** 导航
组下(与 MCP 插件的 API Keys 并列)。`read`/`update`/`delete` 由
`adminAccess` 控制;始终拒绝 `create`(客户端通过 DCR 自行注册,tokens 由
token endpoint 生成)。`oauth-auth-codes` 和 `oauth-csrf-nonces` 保持
隐藏并完全锁定。
默认的 `adminAccess` 授权**在你的 `userCollection` 中**的任何已验证用户,
并拒绝公共 REST/GraphQL 接口 —— 这对于
标准入门应用是正确的,其中 `users` 仅包含操作员。**如果你的 `userCollection`
混合了管理员和不受信任的最终用户,请传递你自己的规则:**
```
payloadMcpOAuth({
issuer,
mcpPluginOptions: mcpOptions,
adminAccess: ({ req }) => req.user?.role === 'admin',
})
```
### 添加的 Endpoints
`GET /.well-known/oauth-authorization-server`, `GET /.well-known/oauth-protected-resource`,
`POST /api/oauth/register`, `GET /api/oauth/authorize`, `POST /api/oauth/consent`,
`POST /api/oauth/token`, `POST /api/oauth/revoke`.
OAuth tokens 使用 `pmoauth_` 前缀。MCP 处理程序检查 Bearer 值:
`pmoauth_…` 走 OAuth 路径;其他任何内容则原封不动地委托给原始的 API-key
处理程序。
## 故障排除
| 症状 | 可能原因 |
|---|---|
| `Error: payloadMcpOAuth must be registered AFTER mcpPlugin()` | 插件顺序 —— 将 `payloadMcpOAuth()` 放在 `mcpPlugin()` 之后。 |
| OAuth tokens 报 401 错误,但 API keys 正常工作 | `mcpPluginOptions` 不是**相同**的对象引用(步骤 2)。 |
| `/.well-known/...` 返回应用的 HTML / 404 | 缺少 `proxy.ts` / `middleware.ts` 或其 `matcher` 未包含 well-known 路径(步骤 3)。 |
| **所有**路由均报 500 错误;日志提示 *"can't recognize the exported `config` field … it mustn't be reexported"* | `config` 是从 `…/middleware` 重新导出的,而不是在你的 `proxy.ts` / `middleware.ts` 中声明为本地字面量(步骤 3)。 |
| 出现 `The "middleware" file convention is deprecated` 警告 (Next 16) | 将 `src/middleware.ts` 重命名为 `src/proxy.ts` 并将处理程序导出为 `proxy`(步骤 3)。 |
| 同意屏幕正常显示,但 **Approve** 返回 `401 access_denied / "Authentication required"` | **≤ 0.3.0** 版本中的插件 bug:同意页面发送了 `Referrer-Policy: no-referrer`,因此浏览器在 Approve 的 POST 请求中发送了 `Origin: null`,而 Payload 丢弃了会话(`GET` 渲染没有 `Origin`,所以它正常工作;但 `POST` 不行)。**在 0.3.1 中已修复 —— 请升级。** 如果在 ≥ 0.3.1 版本中仍然存在,说明你的 `serverURL` 与浏览器使用的源不匹配 —— 请检查 `NEXT_PUBLIC_SERVER_URL`(确切的 scheme + host,没有尾部斜杠)。 |
| 管理导航中缺少 **OAuth Clients / OAuth Tokens**,或者它们的面板显示 *"Nothing found"* | 登录用户未被 `adminAccess` 授权。默认情况下,他们必须属于 `userCollection`;对于混合角色的应用,请传递自定义的 `adminAccess`(参见 *管理界面与访问*)。 |
| `migrate` 失败并提示 *"table … already exists"* | 你对已经由 dev push 创建的数据库运行了 `migrate` —— 请选择一种工作流(步骤 6)。 |
| 在 `pnpm dev` 中重建 `payload_locked_documents_rels` 时出现 `SQLITE_ERROR: no such column: oauth_clients_id` | SQLite push 无法将新 collections 的 lock-FK 列添加到**已经推送**的数据库中(Payload/drizzle 重建的一个怪癖)。**在 0.3.2 中已修复** —— OAuth collections 设置了 `lockDocuments: false`,因此它们不在那里添加任何列。在 ≤ 0.3.1 版本上:在首次启动*之前*添加插件,或者重置开发数据库(`rm your.db*`)以便全新创建 schema。 |
| 在开发环境中启动正常,但在部署时报错 | 在生产环境中未设置 `PMOAUTH_TOKEN_PEPPER`(步骤 4)。 |
## 开发
此 repo 是一个 pnpm workspace:已发布的插件位于 `packages/plugin` 中,而
`examples/payload-app` 是一个参考的 Payload 3 应用(SQLite用于集成
测试。
```
pnpm install # once, at the repo root
pnpm dev:example # run the reference example app (workspace source)
pnpm --filter ./packages/plugin build # build the plugin
pnpm test # unit tests across the workspace
pnpm typecheck # type-check
pnpm lint # lint
```
### 从打包的插件启动测试站点
`test:install:serve` 将**打包**的插件(`pnpm pack`,即真正
发布的构件)安装到一个临时应用中,完全按照上述文档说明进行连接,并保持 `next dev` 运行,以便你可以四处点击体验 —— 包括
OAuth 管理界面。
```
pnpm test:install:serve # http://localhost:3000
pnpm test:install:serve -- --port 4000
pnpm test:install:serve -- --reuse # skip the rebuild, reuse the last install (fast restart)
pnpm test:install:serve -- --live # expose a public HTTPS URL (Cloudflare tunnel) for Claude.ai
```
`--live` 会打开一个 Cloudflare 快速隧道(需要 [`cloudflared`](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/);
无需登录),将该公共 HTTPS URL 配置为 OAuth 颁发者和 Payload
`serverURL`,并打印将站点添加为 **Claude.ai 中的 Custom Connector** 的步骤 —
这样你就可以针对你的本地构建驱动真实的 OAuth 连接。
它会打印 URL 以及预设的管理员登录信息(`install-test@example.com` /
`install-test-password-123`)。它是**默认全新**的 —— 每次启动都会从全新打包的插件重新配置,
因此你永远不会点击浏览过时的构建;一旦某次启动成功,可以传递
`--reuse` 以实现更快的重启。首次运行速度很慢(完整的
`pnpm install` + 冷编译 —— 需要几分钟时间)。按 Ctrl+C 停止。
### 运行全新安装测试
```
pnpm test:install # asserts the full install + OAuth handshake end to end
pnpm test:install -- --keep # keep the temp app on success, for debugging
```
这是驱动打包、管理导入映射、schema 推送、
OAuth + PKCE 握手、管理可见性/访问权限控制、禁用矩阵以及
增量安装的测试工具 —— 请参阅 [`scripts/install-test/README.md`](scripts/install-test/README.md)
了解其检查内容和原因的完整列表。
## License
MIT
由位于英国诺福克的网页设计工作室 [BrainWeb](https://www.brainweb.co.uk/) 构建和维护
标签:Claude, CVE检测, MCP, MITM代理, OAuth2.1, Payload CMS, 自动化攻击