@civilization/deepseek-harness-skill-mcp 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -145,7 +145,11 @@ MCP 工具是提供给模型的工具 schema,不是用户 `/` 命令,因此
145
145
 
146
146
  当前版本不接受 `env`、认证 header、带凭据或 token 查询参数的 URL。请勿把密钥放进工具参数、MCP 地址或提交到仓库。
147
147
 
148
- MCP 对端离线、命令启动失败或首次工具发现失败不会阻止 DSH 启动。stdio 与 Streamable HTTP 使用 Harness MCP client 的后台重连策略;旧版 SSE 当前依赖连接自身,在断线后可通过刷新配置重新加载。设置页在连接恢复并发现工具后更新状态。插件加载时也会把旧版本写入的严格启动配置迁移为非致命启动策略。
148
+ ### MCP 自动重连与周期配置
149
+
150
+ - **重连周期滑块(3–60 秒,默认 7 秒)**:用户在添加或编辑 MCP 服务器时,可使用滑块指定最大重连等待阈值(`reconnectInterval`)。
151
+ - **无限长效重试保底**:解除底层连续 10 次失败后彻底放弃重试的硬限制,改为长效保底探测模式。当服务端离线或重启时,客户端从 500ms 开始指数退避,并在达到设定的阈值后持续周期探测;只要 MCP 服务端恢复,客户端将在设定的阈值时间内自动重新建立连接并热重载工具。
152
+ - **全协议统一管理**:`stdio`、`streamable-http` 与旧版 `sse` 均统一纳入监督重连体系,端点离线或初次发现失败绝不阻塞 DSH 正常启动。
149
153
 
150
154
  ## Agent 工具与 Skill
151
155
 
@@ -196,12 +200,12 @@ pnpm run web
196
200
 
197
201
  浏览器端 `/` 候选缓存失效兼容逻辑会使用公开的 `connection/reset` 客户端事件;服务端通过注入 `webServer` 与 `connection` 双重挂载 `/extensions` 路由并由 Cordis effect 托管生命周期,防止动态请求掉入前端静态资源的 405 fallback。自动测试验证 Host/RPC、HTTP 桥接和浏览器 bundle 构建,但没有在本仓库内启动完整 Harness Web 做端到端菜单点击验证。外部文件改动仍依赖 filesystem provider 先完成 watcher 失效,必要时可在变更后点击管理页“刷新”。
198
202
 
199
- ## Harness 版本兼容
203
+ ## Harness / Cordis 版本兼容
200
204
 
201
- 当前版本在 DeepSeek Harness **0.2.0-rc.2** 上验证:`pnpm test`(33 项)与 `pnpm run build` 通过,并逐项核对了所用公开接口——`tools.register/get/schemas/guard`、`tools/pre-execute` 的 `deny` 决策、`skills.register/snapshot`、`webServer.register(kind: 'prefix')`、服务端 `connection.rpc.handle` 与浏览器端 `connection.rpc.call`、`settings.section` 槽位、`locale.register/bind`、`connection/reset` 事件、客户端 bundle 的 `window.__ModuleLoader__.load({ id, factory })` 格式。
205
+ 当前版本在 DeepSeek Harness **0.2.1-rc.2** 及 Cordis **4.0.5** 上验证:`pnpm test`(35 项全部通过)与 `pnpm run build` 通过,并逐项核对了所用公开接口——`tools.register/get/schemas/guard`、`tools/pre-execute` 的 `deny` 决策、`skills.register/snapshot`、`webServer.register(kind: 'prefix')`、服务端 `connection.rpc.handle` 与浏览器端 `connection.rpc.call`、`settings.section` 槽位、`locale.register/bind`、`connection/reset` 事件、客户端 bundle 的 `window.__ModuleLoader__.load({ id, factory })` 格式。
202
206
 
203
- - `peerDependencies` 只声明**下界**(`@deepseek-ai/dsh-tools: >=0.1.5-rc.2`,无上界),以便跟随 Harness 的快速迭代:Harness 的插件兼容性判定用运行时版本比对这些范围。代价是不再对未来的破坏性变更做“响亮拒绝”,且**不保证前序版本**。
204
- - `@deepseek-ai/dsh-mcp-client` 与 `@deepseek-ai/dsh-skill-filesystem` 作为 `dependencies` 固定在 `0.2.0-rc.2`:插件只用它们的 `Config` 校验/规范化托管配置行(真正的连接由 Harness 自带的同名插件完成)。Schemastery 对象对未知字段**透传**,已实测 0.2.0 新增的 `maxInstructionBytes` 在旧 schema 下也不会被丢弃;但若未来 Harness 改的是必填约束或字段语义,这个固定版本会成为陈旧校验源,届时应同步升级。
207
+ - `peerDependencies` 只声明**下界**(`@deepseek-ai/cordis: ^4.0.2`,`@deepseek-ai/dsh-tools: >=0.1.5-rc.2`,无上界),以便跟随 Harness 的快速迭代:Harness 的插件兼容性判定用运行时版本比对这些范围。
208
+ - `@deepseek-ai/dsh-mcp-client` 与 `@deepseek-ai/dsh-skill-filesystem` 作为 `dependencies` 对齐至 `0.2.1-rc.2`,并对齐 `@deepseek-ai/schemastery: ~3.18.5`。
205
209
  - 工具目录的核心工具名单同样是版本相关数据,见上文“名单维护”。
206
210
 
207
211
  ## 发布
package/lib/client.js CHANGED
@@ -216,7 +216,10 @@ var zh = {
216
216
  noCatalogResults: "\u672A\u627E\u5230\u5339\u914D\u7684\u5DE5\u5177",
217
217
  noCatalogResultsHint: "\u8BF7\u5C1D\u8BD5\u4FEE\u6539\u641C\u7D22\u8BCD\u6216\u9009\u62E9\u5176\u4ED6\u63D2\u4EF6\u5206\u7EC4\u3002",
218
218
  toolProtected: "\u6838\u5FC3\u4FDD\u7559",
219
- toolTakeoverHint: "\u6B64\u5DE5\u5177\u53D7 skill-catalog \u7EDF\u4E00\u7BA1\u7406\u3002"
219
+ toolTakeoverHint: "\u6B64\u5DE5\u5177\u53D7 skill-catalog \u7EDF\u4E00\u7BA1\u7406\u3002",
220
+ reconnectInterval: "\u91CD\u8FDE\u5468\u671F\u9608\u503C",
221
+ reconnectIntervalDesc: "\u670D\u52A1\u7AEF\u65AD\u5F00\u540E\u81EA\u52A8\u63A2\u6D4B\u5E76\u6062\u590D\u8FDE\u63A5\u7684\u6700\u5927\u7B49\u5F85\u65F6\u95F4\uFF08\u8303\u56F4 3\u201360 \u79D2\uFF0C\u9ED8\u8BA4 7 \u79D2\uFF09\u3002",
222
+ seconds: "\u79D2"
220
223
  };
221
224
  var en = {
222
225
  navSkill: "Skills",
@@ -342,11 +345,14 @@ var en = {
342
345
  noCatalogResults: "No matching tools found",
343
346
  noCatalogResultsHint: "Try adjusting your search terms or filter.",
344
347
  toolProtected: "Protected",
345
- toolTakeoverHint: "This tool is managed under skill-catalog."
348
+ toolTakeoverHint: "This tool is managed under skill-catalog.",
349
+ reconnectInterval: "Reconnect Interval",
350
+ reconnectIntervalDesc: "Maximum wait time before retrying dropped connections (range 3\u201360s, default 7s).",
351
+ seconds: "s"
346
352
  };
347
353
 
348
354
  // src/client/styles.css
349
- var styles_default = '.dsh-ext{color:var(--dsw-alias-label-primary);max-width:840px;padding:4px 0 28px;font:inherit}\n.dsh-ext-heading,.dsh-ext-card>header{display:flex;justify-content:space-between;align-items:center;gap:16px}\n.dsh-ext h2{font-size:22px;letter-spacing:-.4px;margin:0 0 8px}.dsh-ext h3{font-size:15px;margin:0;display:flex;align-items:center;gap:8px}\n.dsh-ext p,.dsh-ext small{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.6}.dsh-ext-heading p{margin:0}\n.dsh-ext button{font:inherit;font-size:13px;cursor:pointer;border:1px solid var(--dsw-alias-border-l4);border-radius:9px;background:var(--dsw-alias-bg-layer-2);color:inherit;padding:8px 13px;white-space:nowrap}\n.dsh-ext button:hover{background:var(--dsw-alias-bg-layer-3)}.dsh-ext button:disabled{opacity:.5;cursor:default}\n.dsh-ext :is(button,input,select,textarea):focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:3px}\n.dsh-ext-card{border:1px solid var(--dsw-alias-border-l4);border-radius:16px;margin-top:20px;background:var(--dsw-alias-bg-layer-2);overflow:hidden}\n.dsh-ext-card>header{padding:18px 20px;border-bottom:1px solid var(--dsw-alias-border-l4)}.dsh-ext-count{font-size:11px;font-weight:400;color:var(--dsw-alias-label-tertiary)}\n.dsh-ext-empty{padding:32px 20px}.dsh-ext-empty strong{font-size:14px;font-weight:500}.dsh-ext-empty p{margin:6px 0 0}\n.dsh-ext ul{list-style:none;padding:0;margin:0}.dsh-ext li{display:flex;justify-content:space-between;align-items:center;gap:14px;padding:16px 20px;border-bottom:1px solid var(--dsw-alias-border-l4)}.dsh-ext li:last-child{border:0}\n.dsh-ext-details{display:flex;flex-direction:column;gap:5px;min-width:0}.dsh-ext-details strong{font-size:14px}.dsh-ext-details code{font-size:12px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}\n.dsh-ext-runtime{display:flex;align-items:center;flex-wrap:wrap;gap:7px;font-size:12px;color:var(--dsw-alias-label-tertiary)}\n.dsh-ext-status{width:7px;height:7px;border-radius:50%;background:#858585;box-shadow:0 0 0 3px color-mix(in srgb,#858585 16%,transparent)}\n.dsh-ext-status-available{background:#38a169;box-shadow:0 0 0 3px color-mix(in srgb,#38a169 18%,transparent)}.dsh-ext-status-waiting{background:#d69e2e;box-shadow:0 0 0 3px color-mix(in srgb,#d69e2e 18%,transparent)}\n.dsh-ext-runtime details{width:100%;margin-top:2px}.dsh-ext-runtime summary{width:max-content;cursor:pointer;color:var(--dsw-alias-label-secondary);user-select:none}.dsh-ext-runtime summary:hover{color:var(--dsw-alias-label-primary)}\n.dsh-ext-runtime .dsh-ext-tools{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}.dsh-ext-runtime .dsh-ext-tools li{display:block;border:0;padding:4px 8px;border-radius:6px;background:var(--dsw-alias-bg-layer-3)}\n.dsh-ext-form{padding:20px;border-top:1px solid var(--dsw-alias-border-l4)}.dsh-ext fieldset{border:0;padding:0;margin:0;display:grid;grid-template-columns:1fr 1fr;gap:16px}.dsh-ext label{display:flex;flex-direction:column;gap:7px;font-size:13px}\n.dsh-ext input,.dsh-ext select,.dsh-ext textarea{box-sizing:border-box;min-width:0;width:100%;font:inherit;color:inherit;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l4);border-radius:8px;padding:10px}.dsh-ext textarea{resize:vertical}\n.dsh-ext-actions{display:flex;justify-content:flex-end;gap:8px;grid-column:1/-1}.dsh-ext fieldset>p{grid-column:1/-1;margin:0}.dsh-ext-notice{padding:12px 16px;border:1px solid var(--dsw-alias-border-l4);border-radius:10px}.dsh-ext-footnote{padding:0 4px}\n.dsh-ext-row-actions{display:flex;align-items:center;gap:7px;flex-wrap:wrap;justify-content:flex-end}.dsh-ext .dsh-ext-danger{color:#e98282}.dsh-ext-inline-form{width:100%;padding:14px 0 2px;border:0}.dsh-skill-edit-form{margin-top:12px;padding:14px;border-radius:10px;background:var(--dsw-alias-bg-layer-1)}\n.dsh-confirm{box-sizing:border-box;width:min(430px,calc(100vw - 32px));padding:22px;border:1px solid var(--dsw-alias-border-l4);border-radius:16px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);box-shadow:0 20px 60px rgba(0,0,0,.28);font:inherit}.dsh-confirm::backdrop{background:rgba(10,12,16,.48);backdrop-filter:blur(3px)}\n.dsh-confirm-icon{display:grid;place-items:center;width:34px;height:34px;margin-bottom:16px;border-radius:10px;background:color-mix(in srgb,#e05252 14%,transparent);color:#e98282;font-size:19px;font-weight:700}.dsh-confirm-copy h3{margin:0;font-size:16px}.dsh-confirm-copy p{margin:8px 0 0;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.65}.dsh-confirm-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:22px}.dsh-confirm button{font:inherit;font-size:13px;cursor:pointer;border:1px solid var(--dsw-alias-border-l4);border-radius:9px;background:var(--dsw-alias-bg-layer-2);color:inherit;padding:8px 14px}.dsh-confirm button:hover{background:var(--dsw-alias-bg-layer-3)}.dsh-confirm button:disabled{opacity:.5;cursor:default}.dsh-confirm button:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:3px}.dsh-confirm .dsh-confirm-delete{border-color:color-mix(in srgb,#e05252 45%,var(--dsw-alias-border-l4));background:#b83232;color:#fff}.dsh-confirm .dsh-confirm-delete:hover{background:#c63b3b}\n.dsh-ext-heading-actions{display:flex;gap:8px;align-items:center}.dsh-visually-hidden{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n.dsh-skill-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-top:20px}.dsh-skill-stats>div{display:flex;flex-direction:column;gap:4px;padding:14px 16px;border:1px solid var(--dsw-alias-border-l4);border-radius:12px;background:var(--dsw-alias-bg-layer-2)}.dsh-skill-stats strong{font-size:20px}.dsh-skill-stats span{font-size:12px;color:var(--dsw-alias-label-tertiary)}\n.dsh-skill-search{margin-top:14px}.dsh-skill-search input{padding-left:13px}.dsh-skill-manager>.dsh-ext-form{margin-top:14px;border:1px solid var(--dsw-alias-border-l4);border-radius:14px;background:var(--dsw-alias-bg-layer-2)}\n.dsh-skill-filters{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:10px}.dsh-skill-filters label{gap:5px;font-size:11px;color:var(--dsw-alias-label-tertiary)}.dsh-skill-filters select{padding:8px 10px;color:var(--dsw-alias-label-primary)}\n.dsh-skill-source-manager{margin-top:14px}.dsh-skill-source-manager>summary{cursor:pointer;padding:14px 18px;font-size:14px;font-weight:600;user-select:none}.dsh-skill-source-manager[open]>summary{border-bottom:1px solid var(--dsw-alias-border-l4)}\n.dsh-skill-source-manager li{padding:11px 18px;flex-wrap:wrap}.dsh-skill-source-title{display:flex;align-items:flex-start;gap:11px;min-width:0}.dsh-skill-source-title>.dsh-ext-status{margin-top:6px;flex:0 0 auto}.dsh-skill-source-title>div{display:flex;min-width:0;flex-direction:column;gap:3px}.dsh-skill-source-title code{font-size:11px;color:var(--dsw-alias-label-tertiary);overflow-wrap:anywhere}.dsh-skill-source-title small{font-size:11px;color:#e98282}\n.dsh-skill-library{margin-top:14px}.dsh-skill-group-editor{display:flex;align-items:flex-end;gap:8px}.dsh-skill-group-editor label{flex:1}.dsh-skill-group-editor input{padding:7px 9px}.dsh-skill-group-editor button{padding:7px 10px}\n.dsh-skill-row{display:block!important;padding:15px 18px!important}.dsh-skill-row-main{min-width:0}.dsh-skill-name{display:flex;align-items:center;gap:9px}.dsh-skill-name strong{font-size:14px}.dsh-skill-row-main>p{margin:6px 0 3px}.dsh-skill-row-main>code{font-size:11px;color:var(--dsw-alias-label-tertiary)}\n.dsh-skill-badge{font-size:11px;padding:2px 7px;border-radius:999px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-secondary)}.dsh-skill-badge-group{color:var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 12%,transparent)}.dsh-skill-badge-invalid{color:#e98282}.dsh-skill-badge-shadowed{color:#a98af8}.dsh-ext-status-invalid{background:#e05252;box-shadow:0 0 0 3px color-mix(in srgb,#e05252 18%,transparent)}.dsh-ext-status-shadowed{background:#8b5cf6;box-shadow:0 0 0 3px color-mix(in srgb,#8b5cf6 18%,transparent)}.dsh-ext-status-manual{background:#d69e2e;box-shadow:0 0 0 3px color-mix(in srgb,#d69e2e 18%,transparent)}\n.dsh-skill-row details{margin-top:9px}.dsh-skill-row summary{width:max-content;cursor:pointer;font-size:12px;color:var(--dsw-alias-label-secondary)}.dsh-skill-detail{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:12px;padding:14px;border-radius:10px;background:var(--dsw-alias-bg-layer-1)}.dsh-skill-detail>p{grid-column:1/-1;margin:0}.dsh-skill-detail p strong,.dsh-skill-detail>div>strong{display:block;margin-bottom:5px;color:var(--dsw-alias-label-secondary);font-size:12px}\n.dsh-skill-invocation{display:flex;flex-direction:column;gap:7px}.dsh-skill-invocation label,.dsh-skill-create-flags label{display:flex;flex-direction:row;align-items:center;gap:5px}.dsh-ext input[type=checkbox]{width:auto;min-width:auto}.dsh-skill-detail ul,.dsh-skill-detail li{display:block!important;margin:0;padding:0!important;border:0!important}.dsh-skill-resources{display:flex!important;flex-wrap:wrap!important;gap:5px}.dsh-skill-resources li{padding:3px 6px!important;border-radius:5px!important;background:var(--dsw-alias-bg-layer-3)!important}\n.dsh-ext-span{grid-column:1/-1}.dsh-skill-create-flags{display:flex;flex-direction:column;gap:7px;font-size:13px}.dsh-skill-create-flags>span{margin-bottom:1px}.dsh-skill-source>.dsh-ext-notice{margin:12px 18px}.dsh-skill-source>.dsh-ext-empty{padding:18px}\n@media(max-width:600px){.dsh-ext fieldset,.dsh-skill-detail,.dsh-skill-filters{grid-template-columns:1fr}.dsh-ext-heading{align-items:flex-start;flex-direction:column}.dsh-ext-heading-actions{flex-wrap:wrap}.dsh-ext-card>header{padding:14px}.dsh-ext li{padding:14px}.dsh-skill-stats{grid-template-columns:repeat(2,1fr)}.dsh-skill-group-editor{align-items:stretch;flex-direction:column}}\n\n/* Skill Catalog Takeover Styles */\n.dsh-catalog-section{margin-top:14px}\n.dsh-catalog-header-actions{display:flex;align-items:center;gap:8px}\n.dsh-catalog-group-card{margin-top:16px;border:1px solid var(--dsw-alias-border-l4);border-radius:14px;background:var(--dsw-alias-bg-layer-2);overflow:hidden}\n.dsh-catalog-group-header{display:flex;justify-content:space-between;align-items:center;padding:14px 18px;background:color-mix(in srgb,var(--dsw-alias-bg-layer-3) 50%,transparent);border-bottom:1px solid var(--dsw-alias-border-l4);cursor:pointer;user-select:none}\n.dsh-catalog-group-header::-webkit-details-marker{display:none}\n.dsh-catalog-group-header::marker{content:""}\n.dsh-catalog-group-card:not([open]) .dsh-catalog-group-header{border-bottom:0}\n.dsh-catalog-group-title{display:flex;align-items:center;gap:10px}\n.dsh-catalog-group-title::before{content:"\u25B6";display:inline-block;font-size:11px;color:var(--dsw-alias-label-tertiary);transition:transform .2s ease}\n.dsh-catalog-group-card[open] .dsh-catalog-group-title::before{transform:rotate(90deg)}\n.dsh-catalog-group-title h4{margin:0;font-size:14px;font-weight:600}\n.dsh-catalog-group-actions{display:flex;align-items:center;gap:6px}\n.dsh-catalog-group-actions button{padding:4px 10px;font-size:12px;border-radius:7px}\n.dsh-catalog-tool-item{display:flex;justify-content:space-between;align-items:center;gap:16px;padding:14px 18px;border-bottom:1px solid var(--dsw-alias-border-l4);transition:background .15s ease}\n.dsh-catalog-tool-item:last-child{border-bottom:0}\n.dsh-catalog-tool-item:hover{background:color-mix(in srgb,var(--dsw-alias-bg-layer-3) 40%,transparent)}\n.dsh-catalog-tool-info{display:flex;flex-direction:column;gap:5px;min-width:0;flex:1}\n.dsh-catalog-tool-header{display:flex;align-items:center;gap:8px;flex-wrap:wrap}\n.dsh-catalog-tool-header code{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:13px;font-weight:600;color:var(--dsw-alias-brand-primary)}\n.dsh-catalog-tool-info p{margin:0;font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.5}\n.dsh-catalog-switch-wrap{display:flex;align-items:center;gap:10px;flex-shrink:0}\n.dsh-catalog-switch-label{font-size:12px;font-weight:500}\n.dsh-catalog-switch{position:relative;display:inline-block;width:40px;height:22px;cursor:pointer}\n.dsh-catalog-switch input{opacity:0;width:0;height:0;position:absolute}\n.dsh-catalog-slider{position:absolute;cursor:pointer;inset:0;background:var(--dsw-alias-bg-layer-4,#3b3b3b);border:1px solid var(--dsw-alias-border-l4);border-radius:22px;transition:.2s}\n.dsh-catalog-slider:before{position:absolute;content:"";height:16px;width:16px;left:2px;bottom:2px;background:#fff;border-radius:50%;transition:.2s}\n.dsh-catalog-switch input:checked + .dsh-catalog-slider{background:var(--dsw-alias-brand-primary,#3182ce);border-color:var(--dsw-alias-brand-primary,#3182ce)}\n.dsh-catalog-switch input:checked + .dsh-catalog-slider:before{transform:translateX(18px)}\n.dsh-catalog-switch input:disabled + .dsh-catalog-slider{opacity:.4;cursor:not-allowed}\n.dsh-catalog-tool-details{margin-top:4px;width:100%}\n.dsh-catalog-tool-details summary{width:max-content;cursor:pointer;font-size:12px;color:var(--dsw-alias-label-secondary);user-select:none;transition:color .15s}\n.dsh-catalog-tool-details summary:hover{color:var(--dsw-alias-label-primary)}\n.dsh-catalog-tool-details p{margin:6px 0 2px;font-size:12px;color:var(--dsw-alias-label-tertiary);line-height:1.55;background:var(--dsw-alias-bg-layer-1);padding:8px 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l4)}\n\n';
355
+ var styles_default = '.dsh-ext{color:var(--dsw-alias-label-primary);max-width:840px;padding:4px 0 28px;font:inherit}\n.dsh-ext-heading,.dsh-ext-card>header{display:flex;justify-content:space-between;align-items:center;gap:16px}\n.dsh-ext h2{font-size:22px;letter-spacing:-.4px;margin:0 0 8px}.dsh-ext h3{font-size:15px;margin:0;display:flex;align-items:center;gap:8px}\n.dsh-ext p,.dsh-ext small{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.6}.dsh-ext-heading p{margin:0}\n.dsh-ext button{font:inherit;font-size:13px;cursor:pointer;border:1px solid var(--dsw-alias-border-l4);border-radius:9px;background:var(--dsw-alias-bg-layer-2);color:inherit;padding:8px 13px;white-space:nowrap}\n.dsh-ext button:hover{background:var(--dsw-alias-bg-layer-3)}.dsh-ext button:disabled{opacity:.5;cursor:default}\n.dsh-ext :is(button,input,select,textarea):focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:3px}\n.dsh-ext-card{border:1px solid var(--dsw-alias-border-l4);border-radius:16px;margin-top:20px;background:var(--dsw-alias-bg-layer-2);overflow:hidden}\n.dsh-ext-card>header{padding:18px 20px;border-bottom:1px solid var(--dsw-alias-border-l4)}.dsh-ext-count{font-size:11px;font-weight:400;color:var(--dsw-alias-label-tertiary)}\n.dsh-ext-empty{padding:32px 20px}.dsh-ext-empty strong{font-size:14px;font-weight:500}.dsh-ext-empty p{margin:6px 0 0}\n.dsh-ext ul{list-style:none;padding:0;margin:0}.dsh-ext li{display:flex;justify-content:space-between;align-items:center;gap:14px;padding:16px 20px;border-bottom:1px solid var(--dsw-alias-border-l4)}.dsh-ext li:last-child{border:0}\n.dsh-ext-details{display:flex;flex-direction:column;gap:5px;min-width:0}.dsh-ext-details strong{font-size:14px}.dsh-ext-details code{font-size:12px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}\n.dsh-ext-runtime{display:flex;align-items:center;flex-wrap:wrap;gap:7px;font-size:12px;color:var(--dsw-alias-label-tertiary)}\n.dsh-ext-status{width:7px;height:7px;border-radius:50%;background:#858585;box-shadow:0 0 0 3px color-mix(in srgb,#858585 16%,transparent)}\n.dsh-ext-status-available{background:#38a169;box-shadow:0 0 0 3px color-mix(in srgb,#38a169 18%,transparent)}.dsh-ext-status-waiting{background:#d69e2e;box-shadow:0 0 0 3px color-mix(in srgb,#d69e2e 18%,transparent)}\n.dsh-ext-runtime details{width:100%;margin-top:2px}.dsh-ext-runtime summary{width:max-content;cursor:pointer;color:var(--dsw-alias-label-secondary);user-select:none}.dsh-ext-runtime summary:hover{color:var(--dsw-alias-label-primary)}\n.dsh-ext-runtime .dsh-ext-tools{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}.dsh-ext-runtime .dsh-ext-tools li{display:block;border:0;padding:4px 8px;border-radius:6px;background:var(--dsw-alias-bg-layer-3)}\n.dsh-ext-form{padding:20px;border-top:1px solid var(--dsw-alias-border-l4)}.dsh-ext fieldset{border:0;padding:0;margin:0;display:grid;grid-template-columns:1fr 1fr;gap:16px}.dsh-ext label{display:flex;flex-direction:column;gap:7px;font-size:13px}\n.dsh-ext input,.dsh-ext select,.dsh-ext textarea{box-sizing:border-box;min-width:0;width:100%;font:inherit;color:inherit;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l4);border-radius:8px;padding:10px}.dsh-ext textarea{resize:vertical}\n.dsh-ext-actions{display:flex;justify-content:flex-end;gap:8px;grid-column:1/-1}.dsh-ext fieldset>p{grid-column:1/-1;margin:0}.dsh-ext-notice{padding:12px 16px;border:1px solid var(--dsw-alias-border-l4);border-radius:10px}.dsh-ext-footnote{padding:0 4px}\n.dsh-ext-row-actions{display:flex;align-items:center;gap:7px;flex-wrap:wrap;justify-content:flex-end}.dsh-ext .dsh-ext-danger{color:#e98282}.dsh-ext-inline-form{width:100%;padding:14px 0 2px;border:0}.dsh-skill-edit-form{margin-top:12px;padding:14px;border-radius:10px;background:var(--dsw-alias-bg-layer-1)}\n.dsh-confirm{box-sizing:border-box;width:min(430px,calc(100vw - 32px));padding:22px;border:1px solid var(--dsw-alias-border-l4);border-radius:16px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);box-shadow:0 20px 60px rgba(0,0,0,.28);font:inherit}.dsh-confirm::backdrop{background:rgba(10,12,16,.48);backdrop-filter:blur(3px)}\n.dsh-confirm-icon{display:grid;place-items:center;width:34px;height:34px;margin-bottom:16px;border-radius:10px;background:color-mix(in srgb,#e05252 14%,transparent);color:#e98282;font-size:19px;font-weight:700}.dsh-confirm-copy h3{margin:0;font-size:16px}.dsh-confirm-copy p{margin:8px 0 0;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.65}.dsh-confirm-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:22px}.dsh-confirm button{font:inherit;font-size:13px;cursor:pointer;border:1px solid var(--dsw-alias-border-l4);border-radius:9px;background:var(--dsw-alias-bg-layer-2);color:inherit;padding:8px 14px}.dsh-confirm button:hover{background:var(--dsw-alias-bg-layer-3)}.dsh-confirm button:disabled{opacity:.5;cursor:default}.dsh-confirm button:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:3px}.dsh-confirm .dsh-confirm-delete{border-color:color-mix(in srgb,#e05252 45%,var(--dsw-alias-border-l4));background:#b83232;color:#fff}.dsh-confirm .dsh-confirm-delete:hover{background:#c63b3b}\n.dsh-ext-heading-actions{display:flex;gap:8px;align-items:center}.dsh-visually-hidden{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n.dsh-skill-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-top:20px}.dsh-skill-stats>div{display:flex;flex-direction:column;gap:4px;padding:14px 16px;border:1px solid var(--dsw-alias-border-l4);border-radius:12px;background:var(--dsw-alias-bg-layer-2)}.dsh-skill-stats strong{font-size:20px}.dsh-skill-stats span{font-size:12px;color:var(--dsw-alias-label-tertiary)}\n.dsh-skill-search{margin-top:14px}.dsh-skill-search input{padding-left:13px}.dsh-skill-manager>.dsh-ext-form{margin-top:14px;border:1px solid var(--dsw-alias-border-l4);border-radius:14px;background:var(--dsw-alias-bg-layer-2)}\n.dsh-skill-filters{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:10px}.dsh-skill-filters label{gap:5px;font-size:11px;color:var(--dsw-alias-label-tertiary)}.dsh-skill-filters select{padding:8px 10px;color:var(--dsw-alias-label-primary)}\n.dsh-skill-source-manager{margin-top:14px}.dsh-skill-source-manager>summary{cursor:pointer;padding:14px 18px;font-size:14px;font-weight:600;user-select:none}.dsh-skill-source-manager[open]>summary{border-bottom:1px solid var(--dsw-alias-border-l4)}\n.dsh-skill-source-manager li{padding:11px 18px;flex-wrap:wrap}.dsh-skill-source-title{display:flex;align-items:flex-start;gap:11px;min-width:0}.dsh-skill-source-title>.dsh-ext-status{margin-top:6px;flex:0 0 auto}.dsh-skill-source-title>div{display:flex;min-width:0;flex-direction:column;gap:3px}.dsh-skill-source-title code{font-size:11px;color:var(--dsw-alias-label-tertiary);overflow-wrap:anywhere}.dsh-skill-source-title small{font-size:11px;color:#e98282}\n.dsh-skill-library{margin-top:14px}.dsh-skill-group-editor{display:flex;align-items:flex-end;gap:8px}.dsh-skill-group-editor label{flex:1}.dsh-skill-group-editor input{padding:7px 9px}.dsh-skill-group-editor button{padding:7px 10px}\n.dsh-skill-row{display:block!important;padding:15px 18px!important}.dsh-skill-row-main{min-width:0}.dsh-skill-name{display:flex;align-items:center;gap:9px}.dsh-skill-name strong{font-size:14px}.dsh-skill-row-main>p{margin:6px 0 3px}.dsh-skill-row-main>code{font-size:11px;color:var(--dsw-alias-label-tertiary)}\n.dsh-skill-badge{font-size:11px;padding:2px 7px;border-radius:999px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-secondary)}.dsh-skill-badge-group{color:var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 12%,transparent)}.dsh-skill-badge-invalid{color:#e98282}.dsh-skill-badge-shadowed{color:#a98af8}.dsh-ext-status-invalid{background:#e05252;box-shadow:0 0 0 3px color-mix(in srgb,#e05252 18%,transparent)}.dsh-ext-status-shadowed{background:#8b5cf6;box-shadow:0 0 0 3px color-mix(in srgb,#8b5cf6 18%,transparent)}.dsh-ext-status-manual{background:#d69e2e;box-shadow:0 0 0 3px color-mix(in srgb,#d69e2e 18%,transparent)}\n.dsh-skill-row details{margin-top:9px}.dsh-skill-row summary{width:max-content;cursor:pointer;font-size:12px;color:var(--dsw-alias-label-secondary)}.dsh-skill-detail{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:12px;padding:14px;border-radius:10px;background:var(--dsw-alias-bg-layer-1)}.dsh-skill-detail>p{grid-column:1/-1;margin:0}.dsh-skill-detail p strong,.dsh-skill-detail>div>strong{display:block;margin-bottom:5px;color:var(--dsw-alias-label-secondary);font-size:12px}\n.dsh-skill-invocation{display:flex;flex-direction:column;gap:7px}.dsh-skill-invocation label,.dsh-skill-create-flags label{display:flex;flex-direction:row;align-items:center;gap:5px}.dsh-ext input[type=checkbox]{width:auto;min-width:auto}.dsh-skill-detail ul,.dsh-skill-detail li{display:block!important;margin:0;padding:0!important;border:0!important}.dsh-skill-resources{display:flex!important;flex-wrap:wrap!important;gap:5px}.dsh-skill-resources li{padding:3px 6px!important;border-radius:5px!important;background:var(--dsw-alias-bg-layer-3)!important}\n.dsh-ext-span{grid-column:1/-1}.dsh-skill-create-flags{display:flex;flex-direction:column;gap:7px;font-size:13px}.dsh-skill-create-flags>span{margin-bottom:1px}.dsh-skill-source>.dsh-ext-notice{margin:12px 18px}.dsh-skill-source>.dsh-ext-empty{padding:18px}\n@media(max-width:600px){.dsh-ext fieldset,.dsh-skill-detail,.dsh-skill-filters{grid-template-columns:1fr}.dsh-ext-heading{align-items:flex-start;flex-direction:column}.dsh-ext-heading-actions{flex-wrap:wrap}.dsh-ext-card>header{padding:14px}.dsh-ext li{padding:14px}.dsh-skill-stats{grid-template-columns:repeat(2,1fr)}.dsh-skill-group-editor{align-items:stretch;flex-direction:column}}\n\n/* Skill Catalog Takeover Styles */\n.dsh-catalog-section{margin-top:14px}\n.dsh-catalog-header-actions{display:flex;align-items:center;gap:8px}\n.dsh-catalog-group-card{margin-top:16px;border:1px solid var(--dsw-alias-border-l4);border-radius:14px;background:var(--dsw-alias-bg-layer-2);overflow:hidden}\n.dsh-catalog-group-header{display:flex;justify-content:space-between;align-items:center;padding:14px 18px;background:color-mix(in srgb,var(--dsw-alias-bg-layer-3) 50%,transparent);border-bottom:1px solid var(--dsw-alias-border-l4);cursor:pointer;user-select:none}\n.dsh-catalog-group-header::-webkit-details-marker{display:none}\n.dsh-catalog-group-header::marker{content:""}\n.dsh-catalog-group-card:not([open]) .dsh-catalog-group-header{border-bottom:0}\n.dsh-catalog-group-title{display:flex;align-items:center;gap:10px}\n.dsh-catalog-group-title::before{content:"\u25B6";display:inline-block;font-size:11px;color:var(--dsw-alias-label-tertiary);transition:transform .2s ease}\n.dsh-catalog-group-card[open] .dsh-catalog-group-title::before{transform:rotate(90deg)}\n.dsh-catalog-group-title h4{margin:0;font-size:14px;font-weight:600}\n.dsh-catalog-group-actions{display:flex;align-items:center;gap:6px}\n.dsh-catalog-group-actions button{padding:4px 10px;font-size:12px;border-radius:7px}\n.dsh-catalog-tool-item{display:flex;justify-content:space-between;align-items:center;gap:16px;padding:14px 18px;border-bottom:1px solid var(--dsw-alias-border-l4);transition:background .15s ease}\n.dsh-catalog-tool-item:last-child{border-bottom:0}\n.dsh-catalog-tool-item:hover{background:color-mix(in srgb,var(--dsw-alias-bg-layer-3) 40%,transparent)}\n.dsh-catalog-tool-info{display:flex;flex-direction:column;gap:5px;min-width:0;flex:1}\n.dsh-catalog-tool-header{display:flex;align-items:center;gap:8px;flex-wrap:wrap}\n.dsh-catalog-tool-header code{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:13px;font-weight:600;color:var(--dsw-alias-brand-primary)}\n.dsh-catalog-tool-info p{margin:0;font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.5}\n.dsh-catalog-switch-wrap{display:flex;align-items:center;gap:10px;flex-shrink:0}\n.dsh-catalog-switch-label{font-size:12px;font-weight:500}\n.dsh-catalog-switch{position:relative;display:inline-block;width:40px;height:22px;cursor:pointer}\n.dsh-catalog-switch input{opacity:0;width:0;height:0;position:absolute}\n.dsh-catalog-slider{position:absolute;cursor:pointer;inset:0;background:var(--dsw-alias-bg-layer-4,#3b3b3b);border:1px solid var(--dsw-alias-border-l4);border-radius:22px;transition:.2s}\n.dsh-catalog-slider:before{position:absolute;content:"";height:16px;width:16px;left:2px;bottom:2px;background:#fff;border-radius:50%;transition:.2s}\n.dsh-catalog-switch input:checked + .dsh-catalog-slider{background:var(--dsw-alias-brand-primary,#3182ce);border-color:var(--dsw-alias-brand-primary,#3182ce)}\n.dsh-catalog-switch input:checked + .dsh-catalog-slider:before{transform:translateX(18px)}\n.dsh-catalog-switch input:disabled + .dsh-catalog-slider{opacity:.4;cursor:not-allowed}\n.dsh-catalog-tool-details{margin-top:4px;width:100%}\n.dsh-catalog-tool-details summary{width:max-content;cursor:pointer;font-size:12px;color:var(--dsw-alias-label-secondary);user-select:none;transition:color .15s}\n.dsh-catalog-tool-details summary:hover{color:var(--dsw-alias-label-primary)}\n.dsh-catalog-tool-details p{margin:6px 0 2px;font-size:12px;color:var(--dsw-alias-label-tertiary);line-height:1.55;background:var(--dsw-alias-bg-layer-1);padding:8px 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l4)}\n\n/* MCP Reconnect Slider */\n.dsh-mcp-slider-group{grid-column:1/-1;display:flex;flex-direction:column;gap:8px;padding:12px 14px;border-radius:10px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l4)}\n.dsh-mcp-slider-header{display:flex;justify-content:space-between;align-items:center}\n.dsh-mcp-slider-header label{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary)}\n.dsh-mcp-slider-badge{font-size:12px;font-weight:700;padding:2px 8px;border-radius:999px;color:var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 14%,transparent)}\n.dsh-mcp-slider-group input[type=range]{width:100%;cursor:pointer;accent-color:var(--dsw-alias-brand-primary);margin:4px 0}\n.dsh-mcp-slider-hint{margin:0;font-size:12px;color:var(--dsw-alias-label-tertiary);line-height:1.4}\n\n\n';
350
356
 
351
357
  // src/client/index.jsx
352
358
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -962,8 +968,9 @@ function SkillSourceForm({ source, t, busy, onSave, onCancel }) {
962
968
  }
963
969
  function McpForm({ row, t, busy, onSave, onCancel }) {
964
970
  const prefix = (0, import_react.useId)();
965
- const initial = row?.configuration ?? { transport: "stdio", command: "", args: [], cwd: "" };
971
+ const initial = row?.configuration ?? { transport: "stdio", command: "", args: [], cwd: "", reconnectInterval: 7 };
966
972
  const [transport, setTransport] = (0, import_react.useState)(initial.transport);
973
+ const [reconnectInterval, setReconnectInterval] = (0, import_react.useState)(initial.reconnectInterval ?? 7);
967
974
  const field = (name, required = true, multiline = false, value = "") => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { htmlFor: `${prefix}-${name}`, children: [
968
975
  t(name),
969
976
  multiline ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("textarea", { id: `${prefix}-${name}`, name, rows: 3, defaultValue: value }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { id: `${prefix}-${name}`, name, required, autoComplete: "off", defaultValue: value, ...name === "id" ? { pattern: "[A-Za-z0-9_\\-]{1,32}", maxLength: 32 } : {} })
@@ -971,7 +978,7 @@ function McpForm({ row, t, busy, onSave, onCancel }) {
971
978
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("form", { className: row ? "dsh-ext-inline-form" : "dsh-ext-form", onSubmit: (event) => {
972
979
  event.preventDefault();
973
980
  const data = Object.fromEntries(new FormData(event.currentTarget));
974
- const configuration = JSON.stringify(transport === "stdio" ? { transport, command: data.command, args: data.args.split(/\r?\n/).filter(Boolean), cwd: data.cwd } : { transport, url: data.url });
981
+ const configuration = JSON.stringify(transport === "stdio" ? { transport, command: data.command, args: data.args.split(/\r?\n/).filter(Boolean), cwd: data.cwd, reconnectInterval } : { transport, url: data.url, reconnectInterval });
975
982
  void onSave({ id: row?.id ?? data.id, configuration });
976
983
  }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("fieldset", { disabled: busy, children: [
977
984
  !row && field("id"),
@@ -988,6 +995,29 @@ function McpForm({ row, t, busy, onSave, onCancel }) {
988
995
  field("args", false, true, (initial.args ?? []).join("\n")),
989
996
  field("cwd", false, false, initial.cwd)
990
997
  ] }) : field("url", true, false, initial.url),
998
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-mcp-slider-group", children: [
999
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-mcp-slider-header", children: [
1000
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { htmlFor: `${prefix}-reconnect`, children: t("reconnectInterval") }),
1001
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "dsh-mcp-slider-badge", children: [
1002
+ reconnectInterval,
1003
+ " ",
1004
+ t("seconds")
1005
+ ] })
1006
+ ] }),
1007
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1008
+ "input",
1009
+ {
1010
+ id: `${prefix}-reconnect`,
1011
+ type: "range",
1012
+ min: "3",
1013
+ max: "60",
1014
+ step: "1",
1015
+ value: reconnectInterval,
1016
+ onChange: (event) => setReconnectInterval(Number(event.target.value))
1017
+ }
1018
+ ),
1019
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dsh-mcp-slider-hint", children: t("reconnectIntervalDesc") })
1020
+ ] }),
991
1021
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: t("secret") }),
992
1022
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ext-actions", children: [
993
1023
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: onCancel, children: t("cancel") }),
@@ -998,6 +1028,7 @@ function McpForm({ row, t, busy, onSave, onCancel }) {
998
1028
  function AddForm({ kind, t, busy, onSave, onCancel }) {
999
1029
  const prefix = (0, import_react.useId)();
1000
1030
  const [transport, setTransport] = (0, import_react.useState)("stdio");
1031
+ const [reconnectInterval, setReconnectInterval] = (0, import_react.useState)(7);
1001
1032
  const field = (name, required = true, multiline = false) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { htmlFor: `${prefix}-${name}`, children: [
1002
1033
  t(name),
1003
1034
  multiline ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("textarea", { id: `${prefix}-${name}`, name, rows: 3 }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { id: `${prefix}-${name}`, name, required, autoComplete: "off", ...name === "id" ? { pattern: "[A-Za-z0-9_\\-]{1,32}", maxLength: 32 } : {} })
@@ -1007,7 +1038,7 @@ function AddForm({ kind, t, busy, onSave, onCancel }) {
1007
1038
  const data = Object.fromEntries(new FormData(event.currentTarget));
1008
1039
  const args = kind === "skill" ? { directory: data.directory } : {
1009
1040
  id: data.id,
1010
- configuration: JSON.stringify(transport === "stdio" ? { transport, command: data.command, args: data.args.split(/\r?\n/).filter(Boolean), cwd: data.cwd } : { transport, url: data.url })
1041
+ configuration: JSON.stringify(transport === "stdio" ? { transport, command: data.command, args: data.args.split(/\r?\n/).filter(Boolean), cwd: data.cwd, reconnectInterval } : { transport, url: data.url, reconnectInterval })
1011
1042
  };
1012
1043
  void onSave(args);
1013
1044
  }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("fieldset", { disabled: busy, children: [
@@ -1026,6 +1057,29 @@ function AddForm({ kind, t, busy, onSave, onCancel }) {
1026
1057
  field("args", false, true),
1027
1058
  field("cwd", false)
1028
1059
  ] }) : field("url"),
1060
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-mcp-slider-group", children: [
1061
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-mcp-slider-header", children: [
1062
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { htmlFor: `${prefix}-reconnect`, children: t("reconnectInterval") }),
1063
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "dsh-mcp-slider-badge", children: [
1064
+ reconnectInterval,
1065
+ " ",
1066
+ t("seconds")
1067
+ ] })
1068
+ ] }),
1069
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1070
+ "input",
1071
+ {
1072
+ id: `${prefix}-reconnect`,
1073
+ type: "range",
1074
+ min: "3",
1075
+ max: "60",
1076
+ step: "1",
1077
+ value: reconnectInterval,
1078
+ onChange: (event) => setReconnectInterval(Number(event.target.value))
1079
+ }
1080
+ ),
1081
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dsh-mcp-slider-hint", children: t("reconnectIntervalDesc") })
1082
+ ] }),
1029
1083
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: t("secret") })
1030
1084
  ] }),
1031
1085
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ext-actions", children: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@civilization/deepseek-harness-skill-mcp",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Skill path and MCP server management for DeepSeek Harness Web",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -44,16 +44,17 @@
44
44
  "test": "node --test tests/*.test.js",
45
45
  "build": "node scripts/build.js",
46
46
  "web": "node scripts/web.js",
47
- "setup": "node scripts/setup.js"
47
+ "setup": "node scripts/setup.js",
48
+ "prepack": "pnpm build"
48
49
  },
49
50
  "engines": {
50
51
  "node": ">=22.19.0"
51
52
  },
52
53
  "packageManager": "pnpm@10.33.0",
53
54
  "dependencies": {
54
- "@deepseek-ai/dsh-mcp-client": "0.2.0-rc.2",
55
- "@deepseek-ai/dsh-skill-filesystem": "0.2.0-rc.2",
56
- "@deepseek-ai/schemastery": "3.18.2",
55
+ "@deepseek-ai/dsh-mcp-client": "0.2.1-rc.2",
56
+ "@deepseek-ai/dsh-skill-filesystem": "0.2.1-rc.2",
57
+ "@deepseek-ai/schemastery": "~3.18.5",
57
58
  "@modelcontextprotocol/sdk": "1.29.0",
58
59
  "yaml": "^2.9.0"
59
60
  },
@@ -62,10 +63,10 @@
62
63
  "@deepseek-ai/dsh-tools": ">=0.1.5-rc.2"
63
64
  },
64
65
  "devDependencies": {
65
- "@deepseek-ai/cordis": "4.0.4",
66
- "@deepseek-ai/dsh-skill": "0.2.0-rc.2",
67
- "@deepseek-ai/dsh-system-prompt": "0.2.0-rc.2",
68
- "@deepseek-ai/dsh-tools": "0.2.0-rc.2",
66
+ "@deepseek-ai/cordis": "4.0.5",
67
+ "@deepseek-ai/dsh-skill": "0.2.1-rc.2",
68
+ "@deepseek-ai/dsh-system-prompt": "0.2.1-rc.2",
69
+ "@deepseek-ai/dsh-tools": "0.2.1-rc.2",
69
70
  "esbuild": "^0.28.2",
70
71
  "react": "^19.2.8"
71
72
  }
@@ -343,8 +343,9 @@ function SkillSourceForm({ source, t, busy, onSave, onCancel }) {
343
343
 
344
344
  function McpForm({ row, t, busy, onSave, onCancel }) {
345
345
  const prefix = useId()
346
- const initial = row?.configuration ?? { transport: 'stdio', command: '', args: [], cwd: '' }
346
+ const initial = row?.configuration ?? { transport: 'stdio', command: '', args: [], cwd: '', reconnectInterval: 7 }
347
347
  const [transport, setTransport] = useState(initial.transport)
348
+ const [reconnectInterval, setReconnectInterval] = useState(initial.reconnectInterval ?? 7)
348
349
  const field = (name, required = true, multiline = false, value = '') => <label htmlFor={`${prefix}-${name}`}>{t(name)}{multiline
349
350
  ? <textarea id={`${prefix}-${name}`} name={name} rows={3} defaultValue={value} />
350
351
  : <input id={`${prefix}-${name}`} name={name} required={required} autoComplete="off" defaultValue={value} {...(name === 'id' ? { pattern: '[A-Za-z0-9_\\-]{1,32}', maxLength: 32 } : {})} />}</label>
@@ -352,13 +353,29 @@ function McpForm({ row, t, busy, onSave, onCancel }) {
352
353
  event.preventDefault()
353
354
  const data = Object.fromEntries(new FormData(event.currentTarget))
354
355
  const configuration = JSON.stringify(transport === 'stdio'
355
- ? { transport, command: data.command, args: data.args.split(/\r?\n/).filter(Boolean), cwd: data.cwd }
356
- : { transport, url: data.url })
356
+ ? { transport, command: data.command, args: data.args.split(/\r?\n/).filter(Boolean), cwd: data.cwd, reconnectInterval }
357
+ : { transport, url: data.url, reconnectInterval })
357
358
  void onSave({ id: row?.id ?? data.id, configuration })
358
359
  }}><fieldset disabled={busy}>{!row && field('id')}
359
360
  <label htmlFor={`${prefix}-transport`}>{t('transport')}<select id={`${prefix}-transport`} value={transport} onChange={event => setTransport(event.target.value)}>
360
361
  <option value="stdio">{t('stdio')}</option><option value="streamable-http">{t('http')}</option><option value="sse">{t('sse')}</option></select></label>
361
362
  {transport === 'stdio' ? <>{field('command', true, false, initial.command)}{field('args', false, true, (initial.args ?? []).join('\n'))}{field('cwd', false, false, initial.cwd)}</> : field('url', true, false, initial.url)}
363
+ <div className="dsh-mcp-slider-group">
364
+ <div className="dsh-mcp-slider-header">
365
+ <label htmlFor={`${prefix}-reconnect`}>{t('reconnectInterval')}</label>
366
+ <span className="dsh-mcp-slider-badge">{reconnectInterval} {t('seconds')}</span>
367
+ </div>
368
+ <input
369
+ id={`${prefix}-reconnect`}
370
+ type="range"
371
+ min="3"
372
+ max="60"
373
+ step="1"
374
+ value={reconnectInterval}
375
+ onChange={event => setReconnectInterval(Number(event.target.value))}
376
+ />
377
+ <p className="dsh-mcp-slider-hint">{t('reconnectIntervalDesc')}</p>
378
+ </div>
362
379
  <p>{t('secret')}</p>
363
380
  <div className="dsh-ext-actions"><button type="button" onClick={onCancel}>{t('cancel')}</button><button type="submit">{t(busy ? 'saving' : row ? 'saveChanges' : 'save')}</button></div>
364
381
  </fieldset></form>
@@ -367,6 +384,7 @@ function McpForm({ row, t, busy, onSave, onCancel }) {
367
384
  function AddForm({ kind, t, busy, onSave, onCancel }) {
368
385
  const prefix = useId()
369
386
  const [transport, setTransport] = useState('stdio')
387
+ const [reconnectInterval, setReconnectInterval] = useState(7)
370
388
  const field = (name, required = true, multiline = false) => <label htmlFor={`${prefix}-${name}`}>{t(name)}{multiline
371
389
  ? <textarea id={`${prefix}-${name}`} name={name} rows={3} />
372
390
  : <input id={`${prefix}-${name}`} name={name} required={required} autoComplete="off" {...(name === 'id' ? { pattern: '[A-Za-z0-9_\\-]{1,32}', maxLength: 32 } : {})} />}</label>
@@ -375,14 +393,30 @@ function AddForm({ kind, t, busy, onSave, onCancel }) {
375
393
  const data = Object.fromEntries(new FormData(event.currentTarget))
376
394
  const args = kind === 'skill' ? { directory: data.directory } : {
377
395
  id: data.id, configuration: JSON.stringify(transport === 'stdio'
378
- ? { transport, command: data.command, args: data.args.split(/\r?\n/).filter(Boolean), cwd: data.cwd }
379
- : { transport, url: data.url }),
396
+ ? { transport, command: data.command, args: data.args.split(/\r?\n/).filter(Boolean), cwd: data.cwd, reconnectInterval }
397
+ : { transport, url: data.url, reconnectInterval }),
380
398
  }
381
399
  void onSave(args)
382
400
  }}><fieldset disabled={busy}>{kind === 'skill' ? field('directory') : <>{field('id')}
383
401
  <label htmlFor={`${prefix}-transport`}>{t('transport')}<select id={`${prefix}-transport`} value={transport} onChange={event => setTransport(event.target.value)}>
384
402
  <option value="stdio">{t('stdio')}</option><option value="streamable-http">{t('http')}</option><option value="sse">{t('sse')}</option></select></label>
385
403
  {transport === 'stdio' ? <>{field('command')}{field('args', false, true)}{field('cwd', false)}</> : field('url')}
404
+ <div className="dsh-mcp-slider-group">
405
+ <div className="dsh-mcp-slider-header">
406
+ <label htmlFor={`${prefix}-reconnect`}>{t('reconnectInterval')}</label>
407
+ <span className="dsh-mcp-slider-badge">{reconnectInterval} {t('seconds')}</span>
408
+ </div>
409
+ <input
410
+ id={`${prefix}-reconnect`}
411
+ type="range"
412
+ min="3"
413
+ max="60"
414
+ step="1"
415
+ value={reconnectInterval}
416
+ onChange={event => setReconnectInterval(Number(event.target.value))}
417
+ />
418
+ <p className="dsh-mcp-slider-hint">{t('reconnectIntervalDesc')}</p>
419
+ </div>
386
420
  <p>{t('secret')}</p>
387
421
  </>}
388
422
  <div className="dsh-ext-actions"><button type="button" onClick={onCancel}>{t('cancel')}</button><button type="submit">{t(busy ? 'saving' : 'save')}</button></div>
@@ -51,6 +51,9 @@ export const zh = {
51
51
  noCatalogResultsHint: '请尝试修改搜索词或选择其他插件分组。',
52
52
  toolProtected: '核心保留',
53
53
  toolTakeoverHint: '此工具受 skill-catalog 统一管理。',
54
+ reconnectInterval: '重连周期阈值',
55
+ reconnectIntervalDesc: '服务端断开后自动探测并恢复连接的最大等待时间(范围 3–60 秒,默认 7 秒)。',
56
+ seconds: '秒',
54
57
  }
55
58
  export const en = {
56
59
  navSkill: 'Skills', navMcp: 'MCP',
@@ -104,4 +107,7 @@ export const en = {
104
107
  noCatalogResultsHint: 'Try adjusting your search terms or filter.',
105
108
  toolProtected: 'Protected',
106
109
  toolTakeoverHint: 'This tool is managed under skill-catalog.',
110
+ reconnectInterval: 'Reconnect Interval',
111
+ reconnectIntervalDesc: 'Maximum wait time before retrying dropped connections (range 3–60s, default 7s).',
112
+ seconds: 's',
107
113
  }
@@ -70,3 +70,12 @@
70
70
  .dsh-catalog-tool-details summary:hover{color:var(--dsw-alias-label-primary)}
71
71
  .dsh-catalog-tool-details p{margin:6px 0 2px;font-size:12px;color:var(--dsw-alias-label-tertiary);line-height:1.55;background:var(--dsw-alias-bg-layer-1);padding:8px 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l4)}
72
72
 
73
+ /* MCP Reconnect Slider */
74
+ .dsh-mcp-slider-group{grid-column:1/-1;display:flex;flex-direction:column;gap:8px;padding:12px 14px;border-radius:10px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l4)}
75
+ .dsh-mcp-slider-header{display:flex;justify-content:space-between;align-items:center}
76
+ .dsh-mcp-slider-header label{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary)}
77
+ .dsh-mcp-slider-badge{font-size:12px;font-weight:700;padding:2px 8px;border-radius:999px;color:var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 14%,transparent)}
78
+ .dsh-mcp-slider-group input[type=range]{width:100%;cursor:pointer;accent-color:var(--dsw-alias-brand-primary);margin:4px 0}
79
+ .dsh-mcp-slider-hint{margin:0;font-size:12px;color:var(--dsw-alias-label-tertiary);line-height:1.4}
80
+
81
+
package/src/mcp-sse.js CHANGED
@@ -18,12 +18,26 @@ export const Config = Schema.object({
18
18
  url: Schema.string().required(),
19
19
  toolCallTimeoutMs: Schema.number().default(60_000),
20
20
  failOnStartupError: Schema.boolean().default(false),
21
+ reconnect: Schema.object({
22
+ enabled: Schema.boolean().default(true),
23
+ initialDelayMs: Schema.number().default(500),
24
+ maxDelayMs: Schema.number().default(7000),
25
+ maxAttempts: Schema.number().default(Number.MAX_SAFE_INTEGER),
26
+ }).default({}),
21
27
  })
22
28
 
23
29
  export async function apply(ctx, config) {
24
- const client = new Client({ name: `dsh-sse-${config.serverName}`, version: '1.0.0' }, { capabilities: {} })
25
- const transport = new SSEClientTransport(new URL(config.url))
30
+ const reconnectPolicy = {
31
+ enabled: config.reconnect?.enabled ?? true,
32
+ initialDelayMs: config.reconnect?.initialDelayMs ?? 500,
33
+ maxDelayMs: config.reconnect?.maxDelayMs ?? 7000,
34
+ maxAttempts: config.reconnect?.maxAttempts ?? Number.MAX_SAFE_INTEGER,
35
+ }
36
+
26
37
  let disposed = false
38
+ let reconnectTimer
39
+ let failedAttempts = 0
40
+ let currentGeneration
27
41
  let registrations = new Map()
28
42
  let synchronization = Promise.resolve()
29
43
 
@@ -31,13 +45,25 @@ export async function apply(ctx, config) {
31
45
  for (const dispose of registrations.values()) dispose()
32
46
  registrations = new Map()
33
47
  }
34
- ctx.effect(() => () => {
35
- disposed = true
36
- clearTools()
37
- void client.close().catch(() => {})
38
- }, 'mcp-sse-client.connection')
39
48
 
40
- const synchronizeTools = async () => {
49
+ const scheduleReconnect = (cause) => {
50
+ if (disposed || !reconnectPolicy.enabled) return
51
+ if (reconnectTimer !== undefined) return
52
+ failedAttempts++
53
+ if (failedAttempts > reconnectPolicy.maxAttempts) {
54
+ ctx.logger.error(`mcp-sse-client(${config.serverName}): giving up after ${reconnectPolicy.maxAttempts} failed reconnect attempts`)
55
+ return
56
+ }
57
+ const delayMs = Math.min(reconnectPolicy.maxDelayMs, reconnectPolicy.initialDelayMs * (2 ** Math.min(failedAttempts - 1, 30)))
58
+ ctx.logger.warn(`mcp-sse-client(${config.serverName}): connection lost or failed (${messageOf(cause)}); reconnecting in ${delayMs}ms (attempt ${failedAttempts})`)
59
+ reconnectTimer = setTimeout(() => {
60
+ reconnectTimer = undefined
61
+ void connectGeneration(false)
62
+ }, delayMs)
63
+ reconnectTimer.unref?.()
64
+ }
65
+
66
+ const synchronizeTools = async (client, generation) => {
41
67
  const definitions = new Map()
42
68
  let cursor
43
69
  do {
@@ -52,7 +78,7 @@ export async function apply(ctx, config) {
52
78
  }
53
79
  cursor = response.nextCursor
54
80
  } while (cursor)
55
- if (disposed) return
81
+ if (disposed || generation.closed) return
56
82
  clearTools()
57
83
  const next = new Map()
58
84
  try {
@@ -64,21 +90,60 @@ export async function apply(ctx, config) {
64
90
  }
65
91
  }
66
92
 
67
- client.setNotificationHandler(ToolListChangedNotificationSchema, () => {
68
- synchronization = synchronization.then(synchronizeTools).catch(error => {
69
- ctx.logger.warn(`mcp-sse-client(${config.serverName}): tool refresh failed: ${messageOf(error)}`)
93
+ async function connectGeneration(startup) {
94
+ if (disposed) return
95
+ const client = new Client({ name: `dsh-sse-${config.serverName}`, version: '1.0.0' }, { capabilities: {} })
96
+ const transport = new SSEClientTransport(new URL(config.url))
97
+ const generation = { client, transport, closed: false }
98
+ currentGeneration = generation
99
+
100
+ const onDisconnect = (err) => {
101
+ if (generation.closed || disposed) return
102
+ generation.closed = true
103
+ clearTools()
104
+ void client.close().catch(() => {})
105
+ if (currentGeneration === generation) {
106
+ scheduleReconnect(err ?? new Error('connection closed'))
107
+ }
108
+ }
109
+
110
+ client.onclose = () => onDisconnect()
111
+ client.onerror = (err) => onDisconnect(err)
112
+
113
+ client.setNotificationHandler(ToolListChangedNotificationSchema, () => {
114
+ if (generation.closed || disposed) return
115
+ synchronization = synchronization.then(() => synchronizeTools(client, generation)).catch(error => {
116
+ ctx.logger.warn(`mcp-sse-client(${config.serverName}): tool refresh failed: ${messageOf(error)}`)
117
+ })
70
118
  })
71
- })
72
119
 
73
- try {
74
- await client.connect(transport)
75
- await synchronizeTools()
76
- } catch (error) {
77
- clearTools()
78
- await client.close().catch(() => {})
79
- if (config.failOnStartupError) throw new Error(`mcp-sse-client(${config.serverName}): initial connection failed: ${messageOf(error)}`, { cause: error })
80
- ctx.logger.warn(`mcp-sse-client(${config.serverName}): initial connection failed: ${messageOf(error)}`)
120
+ try {
121
+ await client.connect(transport)
122
+ await synchronizeTools(client, generation)
123
+ failedAttempts = 0
124
+ ctx.logger.info(`mcp-sse-client(${config.serverName}): connected and synchronized tools`)
125
+ } catch (error) {
126
+ onDisconnect(error)
127
+ if (startup && config.failOnStartupError) {
128
+ throw new Error(`mcp-sse-client(${config.serverName}): initial connection failed: ${messageOf(error)}`, { cause: error })
129
+ }
130
+ }
81
131
  }
132
+
133
+ ctx.effect(() => () => {
134
+ disposed = true
135
+ if (reconnectTimer !== undefined) {
136
+ clearTimeout(reconnectTimer)
137
+ reconnectTimer = undefined
138
+ }
139
+ clearTools()
140
+ if (currentGeneration && !currentGeneration.closed) {
141
+ currentGeneration.closed = true
142
+ void currentGeneration.client.close().catch(() => {})
143
+ }
144
+ }, 'mcp-sse-client.connection')
145
+
146
+ await connectGeneration(true)
82
147
  }
83
148
 
84
149
  function createTool(client, publicName, tool, timeout) {
package/src/store.js CHANGED
@@ -39,12 +39,13 @@ function validateId(id) {
39
39
  }
40
40
 
41
41
  function parsePatch(value) {
42
- if (!Array.isArray(value) || value.length !== 1 || !Array.isArray(value[0]?.insert)
43
- || Object.keys(value[0]).join() !== 'insert') throw new Error('Expected one managed insert operation.')
42
+ if (!Array.isArray(value)) throw new Error('Expected patch array.')
43
+ const insertOp = value.find(row => row && typeof row === 'object' && Array.isArray(row.insert))
44
+ if (!insertOp) throw new Error('Expected one managed insert operation.')
44
45
  const ids = new Set()
45
46
  const managed = []
46
47
  const preserved = []
47
- for (const row of value[0].insert) {
48
+ for (const row of insertOp.insert) {
48
49
  if (row?.id === 'extension-manager' && row.name === managerModule) {
49
50
  if (ids.has(row.id)) throw new Error('Duplicate extension id.')
50
51
  ids.add(row.id)
@@ -69,7 +70,8 @@ function parsePatch(value) {
69
70
  } else throw new Error('Unknown managed plugin module.')
70
71
  managed.push(row)
71
72
  }
72
- return { managed, preserved }
73
+ const unmanagedRows = value.filter(row => row !== insertOp)
74
+ return { managed, preserved, unmanagedRows }
73
75
  }
74
76
 
75
77
  /** Validate the patch and return only extension rows owned by the manager. */
@@ -84,7 +86,8 @@ function markedRows(content) {
84
86
  if (start < 0 || end < start || content.indexOf(blockStart, start + blockStart.length) >= 0
85
87
  || content.indexOf(blockEnd, end + blockEnd.length) >= 0) throw new Error('Invalid dsh-skill-mcp managed block.')
86
88
  const bodyStart = start + blockStart.length
87
- return { start, end: end + blockEnd.length, rows: validatePatch(parse(content.slice(bodyStart, end))) }
89
+ const parsed = parsePatch(parse(content.slice(bodyStart, end)))
90
+ return { start, end: end + blockEnd.length, rows: parsed.managed, unmanagedRows: parsed.unmanagedRows }
88
91
  }
89
92
 
90
93
  function legacyPatch(content) {
@@ -99,7 +102,10 @@ function renderBlock(rows) {
99
102
  function writeManagedRows(content, rows) {
100
103
  const marked = markedRows(content)
101
104
  const block = renderBlock(rows)
102
- if (marked) return `${content.slice(0, marked.start)}${block}${content.slice(marked.end)}`
105
+ if (marked) {
106
+ const unmanaged = marked.unmanagedRows?.length ? `\n${stringify(marked.unmanagedRows).trimEnd()}\n` : ''
107
+ return `${content.slice(0, marked.start)}${block}${unmanaged}${content.slice(marked.end)}`
108
+ }
103
109
  const legacy = legacyPatch(content)
104
110
  if (legacy) return `${JSON.stringify([{ insert: [...legacy.preserved, ...rows] }], null, 2)}\n`
105
111
  if (/^[ \t]*\[\][ \t]*$/m.test(content)) return content.replace(/^[ \t]*\[\][ \t]*$/m, block)
@@ -121,10 +127,28 @@ export class ExtensionStore {
121
127
  /** Migrate managed MCP rows so an unavailable server cannot reject profile startup. */
122
128
  async ensureNonFatalMcpStartup() {
123
129
  const rows = await this.read()
124
- if (!rows.some(row => row.config.serverName && row.config.failOnStartupError !== false)) return
130
+ let needsUpdate = false
131
+ for (const row of rows) {
132
+ if (row.config?.serverName) {
133
+ if (row.config.failOnStartupError !== false || !row.config.reconnect || row.config.reconnect.maxAttempts !== Number.MAX_SAFE_INTEGER) {
134
+ needsUpdate = true
135
+ break
136
+ }
137
+ }
138
+ }
139
+ if (!needsUpdate) return
125
140
  await this.update(current => {
126
141
  for (const row of current) {
127
- if (row.config.serverName) row.config.failOnStartupError = false
142
+ if (row.config?.serverName) {
143
+ row.config.failOnStartupError = false
144
+ const maxDelay = row.config.reconnect?.maxDelayMs ?? 7000
145
+ row.config.reconnect = {
146
+ enabled: true,
147
+ initialDelayMs: 500,
148
+ maxDelayMs: Math.min(60_000, Math.max(3_000, maxDelay)),
149
+ maxAttempts: Number.MAX_SAFE_INTEGER,
150
+ }
151
+ }
128
152
  }
129
153
  })
130
154
  }
@@ -195,11 +219,23 @@ export class ExtensionStore {
195
219
  normalizeMcp(id, input) {
196
220
  // Authentication values belong in the host credential setup, not tool arguments or logs.
197
221
  if (input.env || input.headers) throw new Error('This version does not accept environment values or authentication headers.')
222
+ const reconnectIntervalSec = Math.min(60, Math.max(3, Math.round(Number(input.reconnectInterval ?? 7)) || 7))
223
+ const reconnect = {
224
+ enabled: true,
225
+ initialDelayMs: 500,
226
+ maxDelayMs: reconnectIntervalSec * 1000,
227
+ maxAttempts: Number.MAX_SAFE_INTEGER,
228
+ }
198
229
  const normalized = input.transport === 'stdio' ? {
199
230
  ...input,
200
231
  command: typeof input.command === 'string' ? normalizeWindowsPath(input.command) : input.command,
201
232
  cwd: typeof input.cwd === 'string' && input.cwd ? normalizeWindowsPath(input.cwd) : input.cwd,
202
- } : input
233
+ reconnect,
234
+ } : {
235
+ ...input,
236
+ reconnect,
237
+ }
238
+ delete normalized.reconnectInterval
203
239
  const config = input.transport === 'sse'
204
240
  ? SseMcpConfig({ ...normalized, serverName: id, failOnStartupError: false })
205
241
  : McpConfig({ ...normalized, serverName: id, failOnStartupError: false })
package/src/web.js CHANGED
@@ -55,8 +55,18 @@ export async function describeExtensions(store, ctx, options = {}) {
55
55
  : row.config.customSkillDirs[0],
56
56
  transport: row.config.transport ?? '',
57
57
  configuration: row.config.serverName ? (row.config.transport === 'stdio'
58
- ? { transport: row.config.transport, command: row.config.command, args: row.config.args ?? [], cwd: row.config.cwd ?? '' }
59
- : { transport: row.config.transport, url: row.config.url }) : undefined,
58
+ ? {
59
+ transport: row.config.transport,
60
+ command: row.config.command,
61
+ args: row.config.args ?? [],
62
+ cwd: row.config.cwd ?? '',
63
+ reconnectInterval: row.config.reconnect?.maxDelayMs ? Math.round(row.config.reconnect.maxDelayMs / 1000) : 7,
64
+ }
65
+ : {
66
+ transport: row.config.transport,
67
+ url: row.config.url,
68
+ reconnectInterval: row.config.reconnect?.maxDelayMs ? Math.round(row.config.reconnect.maxDelayMs / 1000) : 7,
69
+ }) : undefined,
60
70
  tools: row.config.serverName
61
71
  ? toolNames.filter(name => name.startsWith(`mcp__${row.id}__`)).sort() : [],
62
72
  })),