dsh-mcp-auth
Verifieddsh-mcp-auth · v0.2.1 · MIT · Web UI
Persistent MCP login state for DeepSeek Harness: OAuth 2.1 / manual header credentials per MCP server, injected into mcp-client rows via !!js. Settings UI talks to the host over Typert Remote.
Install
dsh plugin add dsh-mcp-auth Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Published to npm without a public repository. Inspect the package contents before installing.
Tags
Readme
dsh-mcp-auth
Requires: dsh ≥ v0.1.7-rc.2(兼容 0.1.7 / 0.2.x,peer 区间
>=0.1.7-rc.2 <0.3.0;dsh 0.2.1 起有插件版本门禁,需本包 2026-10-20 后版本)。 本包是 dsh 插件,须装进 dsh profile 由 dsh 宿主加载使用,不是独立 npm 库。 桌面端(DeepSeek Harness.app)已支持:设置页「MCP 登录」已改版(卡片/横幅/ 状态圆点,随宿主明暗主题),OAuth 全链路已在桌面 app 0.2.1-rc.2 验证。
为 DeepSeek Harness 提供 MCP 登录态持久化:按服务器保存凭据(OAuth 2.1 令牌
或手工请求头),存放在 dsh home 下,并通过 !!js 配置表达式注入官方
mcp-client 行。不修改任何官方包。
工作原理
- 本 bundle 提供
mcpAuth服务。headers(serverName)同步读取持久化的 登录态(loader 在引导时求值一次)。 - mcp-client 行等待该服务并注入请求头:
- id: my-server
name: '@deepseek-ai/dsh-mcp-client'
inject: [mcpAuth]
config:
transport: streamable-http
serverName: nocodb
url: https://mcp.example.com/mcp
headers: !!js ctx.mcpAuth.headers('nocodb')
- 登录态存于
~/.dsh/mcp-auth/tokens.json(文件0600,目录0700)。
登录方式(设置里的「MCP 登录」分区)
- 手工凭据 —— 粘贴一个请求头(如
Authorization: Bearer …或 API Token 头),原样保存、原样注入。 - OAuth 2.1 —— 基于 MCP SDK 的
auth()状态机:RFC 9728 受保护资源发现、 客户端注册(见下「客户端身份」)、PKCE(S256)、一次性回环重定向监听器、 系统浏览器完成授权。令牌(含refresh_token与绝对expires_at)跨重启持久。
按需登录(懒认证)
长连的 MCP 连接(例如 dsh-mcp-apps 为每个服务器自开的那条)拿到 401 时,SDK 的
streamable-HTTP transport 会自己重跑 auth():不需要人工参与时它直接重试;需要
用户点同意时它抛 UnauthorizedError 交给调用方 —— SDK 自己没法等人。
mcpAuth 因此额外提供:
lazyAuth(serverName, serverUrl, scope?): Promise<LazyAuthHandle>
句柄含 provider(交给 transport 的 authProvider)、authorize()(等人批准后
换码并落盘)、authorizationUrl()、lastError()、dispose()。授权 URL 会写进
list 投影(设置页 "MCP 登录" 分区可见)并打印到 stdout —— 无头环境
(DSH_MCP_AUTH_NO_BROWSER=1)靠它手工打开。
参考实现见 dsh-mcp-apps:把 provider 装到自己的连接上,捕获
UnauthorizedError 后调 authorize(),然后重试那一次调用。
⚠️ 装了 authProvider 就必须把静态 Authorization 从 requestInit.headers 里
去掉。 SDK 的 _commonHeaders() 把 requestInit.headers 展开在 provider 的
bearer 之后,留下的静态头会覆盖刚拿到的令牌,让整条流程静默失效。
死凭据自愈
服务器吊销了会话(或"授权即自杀"场景:受保护的 revoke 工具在登出状态被调用,触发
的新授权又被这次 revoke 本身吊销)后,SDK 的 refresh 路径会抛普通 Error(不是
UnauthorizedError)——不处理的话同一失败会永远重复。authorize() 检测错误消息含
refresh / revoked / invalid_grant 时会自动清掉死凭据并在同一轮内重跑交互式
授权,用户只需再点一次同意。lastError() 始终可查失败原因(不静默)。
另有 reset():只清 OAuth 态、保留手工 header 凭据。
客户端身份(三条路线)
| 路线 | 配置 | 适用对象 |
|---|---|---|
| A · CIMD(SEP-991,URL 型 client id) | clientMetadataUrl:https 且路径非根 |
声明 client_id_metadata_document_supported 的授权服务器 |
| B · 断言式 client id | staticClientId |
既不支持 CIMD 也不支持动态注册的服务器 |
| C · 动态客户端注册 | 两者都不配(默认) | 提供 registration_endpoint 的服务器 |
- 两者都配时 A 优先:可移植的路线不该被遗留的 B 拉回去。
- B 是捷径,不是通用解:它只在对方接受所断言的 id 时可行(例如
lazy-auth-server-demo压根不校验client_id)。换一个真实授权服务器可能被拒。 clientMetadataUrl不合法会让插件启动即报错并带出具体取值。MCP SDK 只接受 https 且路径非根的 URL(isHttpsUrl),没有 localhost 例外,所以回环 http 的元数据文档用不了 —— 本地调试请用 B,或把文档托管到真 HTTPS 上。- 已注册/已发放的
clientInformation优先于配置:刷新时必须沿用当初发放授权 所用的同一个 client id。
配置
bundle 行无必填配置。可选:
| 键 | 默认 | 说明 |
|---|---|---|
tokenFile |
空 | 覆盖存储路径(仅供测试)。 |
clientMetadataUrl |
空 | 路线 A:https 且路径非根的客户端元数据文档 URL。 |
staticClientId |
空 | 路线 B:断言式 client_id。 |
安装
在 profile(~/.dsh/profiles/web/package.json)的 dependencies 中添加:
"dsh-mcp-auth": "^0.2.0"
并把 "dsh-mcp-auth" 列入 dsh.profile.bundles,然后在 profile 目录执行
pnpm install。peer 依赖(@deepseek-ai/cordis、dsh-typert-protocol)无需
手动安装——dsh 宿主自带,Node 会从你的 dsh 安装中解析。
从 0.1.x 的 link: 本地安装升级:把依赖项改成上面的版本号写法,删除
node_modules/里的旧软链(如有),重新pnpm install即可。
安全
- 令牌文件以仅属主权限(
0600)创建。 - 设置界面经 dsh Typert Gateway(
/api通道)调用本插件的 Remote 方法 (mcp-auth命名空间:list / save-headers / forget / oauth-start),受与/api相同的浏览器信任栅栏保护(错误 Origin → 403)。0.2.0 起不再有独立 的 loopback RPC 监听器与 per-boot token。 - OAuth 重定向监听器仅绑定
127.0.0.1,只接受经state校验的回调;懒认证的 监听器在句柄生命周期内常驻(授权请求与令牌交换必须是同一个redirect_uri), 随插件卸载关闭。 - 回调只接受与当前这次授权尝试匹配的
state;不匹配直接 400,不放行。 - 等待用户批准有上限(默认 5 分钟),避免关掉标签页后把调用方永久挂住。
已知限制
headers()在引导时取快照:引导完成后新完成的登录在下次重启(或该行自身 重连)时生效。懒认证不受此限 —— 它的 transport 每次请求都向 provider 取 令牌,授权完成后重试即刻生效。- 读取到已过期且带
refresh_token的令牌时会尽力刷新;刷新结果下次启动生效。 - 暂无交互式刷新界面。
- 走静态头的官方
@deepseek-ai/dsh-mcp-client行不做 401 挑战处理(其产物里401|WWW-Authenticate|oauth|authorize零命中),且不得修改官方包。 解决方式:让官方行disabled: true,由dsh-mcp-apps的工具桥 (registerTools: true)接管工具注册——模型侧调用从此走活凭据连接,免重启。 详见dsh-mcp-apps/README.zh.md的「活凭据工具桥」。
模型体验
不改变任何模型可见面:无新增工具、提示词段落或会话事件;请求与 KV 缓存行为 不变。
开发
pnpm install --ignore-workspace
pnpm run build # tsdown 两遍:node 半边(含 @Remote 装饰器降级)+ client 半边
# 并把手写 Typert 工件(typert.host / typert.remote-client)拷入 lib/
pnpm test
Typert 工件为手写维护(
src/typert.host.manual.js/typert.remote-client.manual.js): 官方 generator 的 package 模式只认 workspace 内的包,不识别从 npm 安装的 protocol。新增/修改@Remote方法时需同步改这两个文件(wire 名 ≠ JS 方法名时 必须写implementation字段)。0.1.x 的 rpc-server / settings.register 路线 已退役(_attic-0.1.x/)。