dsh-mcp-apps
Verified@z-y-q/dsh-mcp-apps · v0.2.0 · MIT · Web UI
MCP Apps display for DeepSeek Harness: renders MCP App UI resources (ui://) inside tool call rows via a separate-origin sandbox.
Install
dsh plugin add @z-y-q/dsh-mcp-apps 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
Creators
Readme
dsh-mcp-apps
Requires: dsh ≥ v0.1.7-rc.2(0.2.0 起只支持 0.1.7 的 Typert Remote 体系; 仍在 0.1.5/0.1.6 的请用 0.1.x 版本)。本包是 dsh 插件,须装进 dsh profile 由 dsh 宿主加载使用,不是独立 npm 库。
为 DeepSeek Harness 提供 MCP Apps 展示:把 MCP 服务器附加在工具上的 ui://
应用资源渲染到 dsh 网页版的工具调用卡片里,使用官方
@modelcontextprotocol/ext-apps 的 AppBridge 与 MCP Apps 规范的双层
iframe 沙箱架构。
官方 @deepseek-ai/dsh-mcp-client 包不会被修改。本 bundle 只读复用其在
loader 里的服务器配置,并自行建立 MCP 连接——因为官方插件会丢弃工具的
_meta,也不提供 resources/read。
安装
把本包加入 dsh profile 并列入 profile 的 dsh.profile.bundles;包内
cordis.patch.yml 会自动插入 mcp-apps 宿主行。
配置
- id: mcp-apps
name: '@z-y-q/dsh-mcp-apps'
config:
servers: [] # mcp-client serverName 白名单;留空表示全部
sandboxPort: 0 # 沙箱源的回环端口;0 表示由系统分配
registerTools: false # 活凭据工具桥开关(见下节);默认关闭
安全模型
- 沙箱代理页由独立的仅回环监听器提供,端口与 dsh 网页服务器不同。必须
使用独立端口:内层 App iframe 带有
allow-same-origin,若与 dsh 同端口, 不受信任的 App HTML 就会与 dsh API 同源。独立端口下,跨源请求主/api会被 harness 信任栅栏拒绝(origin.host !== host authority)。 - CSP 以 HTTP 响应头下发(来自资源声明的域名白名单),绝不使用 meta 标签。
含
;、引号、空格或换行的域名条目会被拒绝。 - 沙箱代理通过 referrer 校验嵌入方(仅回环),拒绝顶层加载;所有转发的 消息都带来源校验。
- App 发起的工具/资源调用经本 bundle 宿主侧转发到自己的 MCP 连接。0.2.0 起
转发通道走 dsh Typert Gateway(
mcp-appsRemote 命名空间:list / resource / call-tool / read-resource / list-resources)——不再有独立的 loopback RPC 端点、per-boot token 与 settings 命名空间引导;沙箱代理页 本身仍在独立回环源上(安全模型不变)。
复用 mcp-client 行的配置(含 !!js)
服务器配置只读复用 loader 里 @deepseek-ai/dsh-mcp-client 的行,读的是该行
fiber 已插值好的配置(entry.fiber.config),因此:
- 行里用
headers: !!js ctx.mcpAuth.headers('x')注入的凭据,会同样出现在本 插件自己的那条连接上——否则需要鉴权的服务器上 App 会静默不显示(只留一条 warn)。 - 若该行尚未启动(没有 fiber),退回读序列化原文
entry.options.config, 并对残留的{__jsExpr}节点尽力求值;求值失败时原样保留并 warn,绝不静默发一个 不带Authorization的请求。
注意 dsh-mcp-auth 的 headers() 在引导时取快照。但那只影响没有装
authProvider 的情况(见下节);装了之后凭据由 provider 每次请求现取,
引导之后新完成的登录立刻生效。
调试:DSH_MCP_APPS_DEBUG=1 时,发现阶段会打印每行的 URL 与请求头名字
(从不打印值),用于判断"这条连接到底带没带凭据"。
按需登录(401 → 弹授权 → 自动重试)
MCP Apps 参考宿主有一条约定:App 里调用受保护工具时,服务器回 401 +
WWW-Authenticate,宿主去跑 OAuth 然后重试。本插件把这半条补上了。
装上 dsh-mcp-auth 时(通过 ctx.get('mcpAuth') 解析,不写进 inject,
所以在没有该 bundle 的 profile 里 App 功能照常可用):
- 建连接时向它要一个懒认证句柄,把
provider交给StreamableHTTPClientTransport的authProvider。 - 同时从静态 headers 里剥掉
Authorization(stripAuthorization)—— 必须这么做:SDK 的_commonHeaders()把requestInit.headers展开在 provider 的 bearer 之后,留下的静态头会覆盖刚拿到的令牌。 - 受保护工具调用失败且异常是
UnauthorizedError时,调authorize()等人批准,成功后重试一次(transport 每次都向 provider 取令牌,所以 重试自然带上新令牌)。
authorize() 返回 FAILED 时抛回原始异常,并在 mcp-auth 侧记录原因
(lastError() / list 的 oauthError)——不静默失败,否则"授权坏了"和
"用户取消了"外观一致。
未装 dsh-mcp-auth 时行为与从前完全一致:401 直接当错误抛出。
活凭据工具桥(registerTools: true)
解决什么:官方 dsh-mcp-client 行的凭据是 !!js 启动时求值一次的快照(loader
语义 + schema 拷贝断引用 + transport 复用同一份 headers,三道闸都无法绕过),所以
启动后新完成的登录/重新授权,模型侧永远看不到,必须重启 dsh。
怎么做:本插件用 ctx.tools.register(公开 API,官方 mcp-client 用的同一个)
把每个服务器的全部工具注册给模型,名字与形状逐字段对齐官方——
mcp__<server>__<tool>(64 字符截断 + 12 位 sha256 后缀)、exec.signal 透传、
isError → throw、content/structuredContent 输出 schema、taskSupport: required
的工具调用时报官方同款错误。因此 dsh-mcp-security 的基线指纹不变,安全门照常生效。
执行路径:execute 走本插件自己的连接(authProvider 活凭据)——每次调用现取
令牌;401/refresh 被拒 → 自动弹授权 → 用户同意 → 重试当次调用。授权失败则原样抛错。
接管方式(二选一):
- 让位模式(推荐):官方行加
disabled: true。行保留——本插件的发现逻辑会读 disabled 行的 url/serverName(fiber.config快照不可用时走options.config+!!js尽力求值的兜底路径)——但官方不再注册工具、不建连接,全部由桥接管。 - 共存模式:官方行保持启用。重名工具桥会跳过并 warn,绝不抢注(注册表
already registered语义),可渐进迁移;想让某服务器彻底切到活凭据时再 disable 那行。
默认关闭:registerTools: false 时不碰工具注册表,行为与本插件早期版本一致。
已知限制
- 展示 App 需要通过回环地址(
127.0.0.1/localhost)访问 GUI;沙箱代理 对局域网浏览器不可达。 - App 发现会为每个已配置的 MCP 服务器建立第二条连接。
- App 的
updateModelContext与ui/message通知被接受但忽略;只提供inline显示模式。 - 工具桥注册的工具不带
_meta(App 资源标记)——与官方注册行为一致;App 卡片 由本插件的发现逻辑另行负责,两者不冲突。 - 仍走官方行(未让位)的服务器,模型侧凭据依旧是启动快照。
- 懒认证句柄各持一个回环监听器,随插件卸载一起关闭(
ctx.effect里 dispose); 工具注册同样在卸载时全部反注册。
模型体验
registerTools: false(默认)时不改变任何模型可见面。registerTools: true 时模型会
看到与官方注册完全同名同形状的工具(唯一的差别在执行路径的凭据是活的),无新增
提示词段落或会话事件,KV 缓存行为不变。
开发
pnpm install --ignore-workspace
pnpm run build # tsdown 两遍:node 半边(含 @Remote 装饰器降级)+ client 半边
# 并把手写 Typert 工件(typert.host / typert.remote-client)拷入 lib/
pnpm test # node --test --test-force-exit test/*.test.mjs test/*.test.ts
Typert 工件为手写维护(
src/typert.host.manual.js/typert.remote-client.manual.js): 官方 generator 的 package 模式只认 workspace 内的包。新增/修改@Remote方法时 需同步改这两个文件(wire 名 ≠ JS 方法名时必须写implementation字段)。 e2e 最小验证:node test/e2e-demo-server.mjs(3927 端口的 ui:// App demo)。
node --test test/会把test/目录本身当模块加载而报MODULE_NOT_FOUND, 所以显式列出用例文件。