Skip to content

dsh-mcp-auth

Verified

dsh-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 行。不修改任何官方包。

工作原理

  1. 本 bundle 提供 mcpAuth 服务。headers(serverName) 同步读取持久化的 登录态(loader 在引导时求值一次)。
  2. 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')
  1. 登录态存于 ~/.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/)。