@civilization/deepseek-harness-skill-mcp 0.1.5 → 0.1.7

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
@@ -2,7 +2,7 @@
2
2
 
3
3
  为 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) Web 界面提供 Skill 路径和 MCP 服务器管理。
4
4
 
5
- 安装后,“设置”中会出现 **Skill 管理** 和 **MCP 管理** 两个页面,同时向 Agent 注册 `extensions_*` 管理工具。插件使用 Harness 自带的 Skill filesystem provider 与 MCP client,配置写入当前 `web` profile 的 `cordis.patch.yml`。
5
+ 安装后,“设置”中会出现 **Skill 管理** 和 **MCP 管理** 两个页面,同时向 Agent 注册 `extensions_*` 管理工具。插件使用 Harness 自带的 Skill filesystem provider 与 MCP client,并为旧版 HTTP+SSE 服务提供兼容客户端;配置写入当前 `web` profile 的 `cordis.patch.yml`。
6
6
 
7
7
  ## 通过 dsh 安装
8
8
 
@@ -71,10 +71,11 @@ Skill 页面支持:
71
71
 
72
72
  ## MCP 管理
73
73
 
74
- 支持两种 MCP 配置类型:
74
+ 支持三种 MCP 配置类型:
75
75
 
76
76
  - `stdio`:执行本地命令并通过标准输入输出通信
77
- - `HTTP`:连接 Streamable HTTP MCP 地址
77
+ - `streamable-http`:连接推荐的 Streamable HTTP MCP 地址
78
+ - `sse`:兼容使用独立 `/sse` 与 `/messages` 端点的旧版 MCP 服务
78
79
 
79
80
  stdio 示例:
80
81
 
@@ -96,13 +97,22 @@ HTTP 示例:
96
97
  }
97
98
  ```
98
99
 
100
+ 旧版 SSE 示例:
101
+
102
+ ```json
103
+ {
104
+ "transport": "sse",
105
+ "url": "http://127.0.0.1:9000/sse"
106
+ }
107
+ ```
108
+
99
109
  服务器卡片使用状态灯显示禁用、等待发现工具或工具可用,并可展开查看实际注册的工具列表。MCP 配置可以编辑或删除,编辑时保留服务器 id 与启用状态。新服务器默认禁用;启用 stdio 服务器会在 Agent 沙箱之外执行所配置的程序。
100
110
 
101
111
  MCP 工具是提供给模型的工具 schema,不是用户 `/` 命令,因此不会出现在 Skill 斜杠菜单中;它们的动态状态以 MCP 页面中的“工具列表”和 `extensions_inspect` 为准。
102
112
 
103
113
  当前版本不接受 `env`、认证 header、带凭据或 token 查询参数的 URL。请勿把密钥放进工具参数、MCP 地址或提交到仓库。
104
114
 
105
- MCP 对端离线、命令启动失败或首次工具发现失败不会阻止 DSH 启动。默认重连策略会在后台重试;设置页在连接恢复并发现工具后更新状态。插件加载时也会把旧版本写入的严格启动配置迁移为非致命启动策略。
115
+ MCP 对端离线、命令启动失败或首次工具发现失败不会阻止 DSH 启动。stdio 与 Streamable HTTP 使用 Harness MCP client 的后台重连策略;旧版 SSE 当前依赖连接自身,在断线后可通过刷新配置重新加载。设置页在连接恢复并发现工具后更新状态。插件加载时也会把旧版本写入的严格启动配置迁移为非致命启动策略。
106
116
 
107
117
  ## Agent 工具
108
118
 
@@ -110,7 +120,7 @@ MCP 对端离线、命令启动失败或首次工具发现失败不会阻止 DSH
110
120
  |---|---|
111
121
  | `extensions_list` | 查看受管 Skill 路径与 MCP 配置 |
112
122
  | `extensions_add_skill` | 添加 Skill 路径;Agent 调用时显式指定内部 id |
113
- | `extensions_add_mcp` | 添加 stdio 或 HTTP MCP 配置 |
123
+ | `extensions_add_mcp` | 添加 stdio、Streamable HTTP 或旧版 SSE MCP 配置 |
114
124
  | `extensions_set_enabled` | 启用或禁用受管配置 |
115
125
  | `extensions_update_skill` | 修改 Skill 来源路径 |
116
126
  | `extensions_update_mcp` | 替换 MCP 连接配置并保留 id 与启用状态 |
package/lib/client.js CHANGED
@@ -30,6 +30,17 @@ module.exports = __toCommonJS(index_exports);
30
30
  var import_react = require("react");
31
31
 
32
32
  // src/client/controller.js
33
+ var skillCatalogMutations = /* @__PURE__ */ new Set([
34
+ "add-skill",
35
+ "update-skill-source",
36
+ "create-skill",
37
+ "update-skill",
38
+ "delete-skill",
39
+ "set-skill-invocation"
40
+ ]);
41
+ function refreshesSlashCatalog(endpoint, options = {}) {
42
+ return options.refreshSlashCatalog === true || skillCatalogMutations.has(endpoint);
43
+ }
33
44
  var ExtensionsController = class {
34
45
  state = { extensions: [], sources: [], skills: [], revision: "", loading: false, saving: false, error: "", saved: false };
35
46
  listeners = /* @__PURE__ */ new Set();
@@ -99,7 +110,8 @@ var zh = {
99
110
  directory: "Skill \u8DEF\u5F84",
100
111
  transport: "\u914D\u7F6E\u7C7B\u578B",
101
112
  stdio: "stdio",
102
- http: "HTTP",
113
+ http: "Streamable HTTP",
114
+ sse: "SSE\uFF08\u65E7\u7248\u517C\u5BB9\uFF09",
103
115
  command: "\u547D\u4EE4",
104
116
  args: "\u53C2\u6570\uFF08\u6BCF\u884C\u4E00\u4E2A\uFF09",
105
117
  cwd: "\u5DE5\u4F5C\u76EE\u5F55\uFF08\u53EF\u9009\uFF09",
@@ -110,10 +122,16 @@ var zh = {
110
122
  create: "\u521B\u5EFA",
111
123
  edit: "\u7F16\u8F91",
112
124
  delete: "\u5220\u9664",
125
+ confirmDelete: "\u786E\u8BA4\u5220\u9664",
126
+ deleting: "\u5220\u9664\u4E2D\u2026",
113
127
  saving: "\u4FDD\u5B58\u4E2D\u2026",
114
128
  loading: "\u52A0\u8F7D\u4E2D\u2026",
115
- deleteExtensionConfirm: "\u786E\u5B9A\u5220\u9664\u8FD9\u4E2A\u914D\u7F6E\u5417\uFF1FSkill \u6765\u6E90\u4E2D\u7684\u6587\u4EF6\u4E0D\u4F1A\u88AB\u5220\u9664\u3002",
116
- deleteSkillConfirm: "\u786E\u5B9A\u6C38\u4E45\u5220\u9664\u8FD9\u4E2A Skill \u53CA\u5176\u76EE\u5F55\u5185\u5BB9\u5417\uFF1F\u6B64\u64CD\u4F5C\u65E0\u6CD5\u64A4\u9500\u3002",
129
+ deleteExtensionTitle: "\u79FB\u9664 Skill \u6765\u6E90",
130
+ deleteMcpTitle: "\u5220\u9664 MCP \u670D\u52A1\u5668",
131
+ deleteSkillTitle: "\u6C38\u4E45\u5220\u9664 Skill",
132
+ deleteExtensionConfirm: "\u5C06\u4ECE\u5F53\u524D\u914D\u7F6E\u4E2D\u79FB\u9664\u201C{name}\u201D\uFF0C\u76EE\u5F55\u4E2D\u7684 Skill \u6587\u4EF6\u4F1A\u4FDD\u7559\u3002",
133
+ deleteMcpConfirm: "\u5C06\u5220\u9664 MCP \u670D\u52A1\u5668\u201C{name}\u201D\u53CA\u5176\u8FDE\u63A5\u914D\u7F6E\u3002",
134
+ deleteSkillConfirm: "\u5C06\u6C38\u4E45\u5220\u9664\u201C{name}\u201D\u53CA\u5176\u76EE\u5F55\u5185\u7684\u8D44\u6E90\u6587\u4EF6\u3002\u6B64\u64CD\u4F5C\u65E0\u6CD5\u64A4\u9500\u3002",
117
135
  enabled: "\u914D\u7F6E\u5DF2\u542F\u7528",
118
136
  disabled: "\u5DF2\u7981\u7528",
119
137
  enable: "\u542F\u7528",
@@ -122,7 +140,7 @@ var zh = {
122
140
  failed: "\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u8F93\u5165\u3001\u8FDE\u63A5\u6216\u670D\u52A1\u7AEF\u65E5\u5FD7\u540E\u91CD\u8BD5\u3002",
123
141
  conflict: "\u914D\u7F6E\u5DF2\u88AB\u4FEE\u6539\uFF0C\u8BF7\u5237\u65B0\u540E\u91CD\u8BD5\u3002\u672A\u63D0\u4EA4\u7684\u8868\u5355\u5185\u5BB9\u5DF2\u4FDD\u7559\u3002",
124
142
  skillFootnote: "\u65B0\u8DEF\u5F84\u9ED8\u8BA4\u7981\u7528\u3002\u542F\u7528\u540E\uFF0CSkill provider \u4F1A\u626B\u63CF\u8BE5\u8DEF\u5F84\u4E0B\u7684\u6240\u6709\u6280\u80FD\u3002\u914D\u7F6E\u53D8\u66F4\u4F1A\u81EA\u52A8\u751F\u6548\u3002",
125
- mcpFootnote: "\u65B0\u670D\u52A1\u5668\u9ED8\u8BA4\u7981\u7528\u3002\u542F\u7528\u540E\u4F1A\u6267\u884C\u6240\u586B\u547D\u4EE4\u6216\u8FDE\u63A5\u6240\u586B\u5730\u5740\u3002\u914D\u7F6E\u53D8\u66F4\u4F1A\u81EA\u52A8\u751F\u6548\u3002",
143
+ mcpFootnote: "\u65B0\u670D\u52A1\u5668\u9ED8\u8BA4\u7981\u7528\u3002\u65B0\u670D\u52A1\u63A8\u8350 Streamable HTTP\uFF1BSSE \u4EC5\u7528\u4E8E\u517C\u5BB9\u65E7\u7248 /sse + /messages \u670D\u52A1\u3002\u914D\u7F6E\u53D8\u66F4\u4F1A\u81EA\u52A8\u751F\u6548\u3002",
126
144
  secret: "\u8BF7\u52FF\u586B\u5199\u5BC6\u94A5\u6216\u5E26\u4EE4\u724C\u7684\u5730\u5740\u3002\u6B64\u7248\u672C\u6682\u4E0D\u652F\u6301\u8BA4\u8BC1\u53C2\u6570\u3002",
127
145
  tools: "\u5DE5\u5177\u5217\u8868",
128
146
  status_available: "\u5DE5\u5177\u53EF\u7528",
@@ -203,7 +221,8 @@ var en = {
203
221
  directory: "Skill path",
204
222
  transport: "Type",
205
223
  stdio: "stdio",
206
- http: "HTTP",
224
+ http: "Streamable HTTP",
225
+ sse: "SSE (legacy)",
207
226
  command: "Command",
208
227
  args: "Arguments (one per line)",
209
228
  cwd: "Working directory (optional)",
@@ -214,10 +233,16 @@ var en = {
214
233
  create: "Create",
215
234
  edit: "Edit",
216
235
  delete: "Delete",
236
+ confirmDelete: "Delete",
237
+ deleting: "Deleting\u2026",
217
238
  saving: "Saving\u2026",
218
239
  loading: "Loading\u2026",
219
- deleteExtensionConfirm: "Delete this configuration? Files inside a Skill source will not be deleted.",
220
- deleteSkillConfirm: "Permanently delete this Skill and its directory contents? This cannot be undone.",
240
+ deleteExtensionTitle: "Remove Skill source",
241
+ deleteMcpTitle: "Delete MCP server",
242
+ deleteSkillTitle: "Permanently delete Skill",
243
+ deleteExtensionConfirm: "Remove \u201C{name}\u201D from this configuration. Skill files in the directory will be kept.",
244
+ deleteMcpConfirm: "Delete MCP server \u201C{name}\u201D and its connection configuration.",
245
+ deleteSkillConfirm: "Permanently delete \u201C{name}\u201D and the resource files in its directory. This cannot be undone.",
221
246
  enabled: "Configured enabled",
222
247
  disabled: "Disabled",
223
248
  enable: "Enable",
@@ -226,7 +251,7 @@ var en = {
226
251
  failed: "Operation failed. Check input, connection or server logs and retry.",
227
252
  conflict: "Configuration changed. Refresh and retry; your draft has been retained.",
228
253
  skillFootnote: "New paths are disabled. Once enabled, the Skill provider scans every skill below them.",
229
- mcpFootnote: "New servers are disabled. Enabling one runs its command or connects to its endpoint; changes apply automatically.",
254
+ mcpFootnote: "New servers are disabled. Prefer Streamable HTTP for new servers; SSE only supports legacy /sse + /messages endpoints.",
230
255
  secret: "Do not enter secrets or token-bearing URLs. Authentication fields are not supported yet.",
231
256
  tools: "Tools",
232
257
  status_available: "Tools available",
@@ -290,7 +315,7 @@ var en = {
290
315
  };
291
316
 
292
317
  // src/client/styles.css
293
- 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-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";
318
+ 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";
294
319
 
295
320
  // src/client/index.jsx
296
321
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -321,7 +346,7 @@ function apply(ctx) {
321
346
  const t = ctx.locale.bind(namespace);
322
347
  const request = async (endpoint, args, options) => {
323
348
  const ok = await controller.request(endpoint, args, options);
324
- if (ok && (endpoint !== "list" || options?.silent !== true)) refreshSlashCatalog();
349
+ if (ok && refreshesSlashCatalog(endpoint, options)) refreshSlashCatalog();
325
350
  return ok;
326
351
  };
327
352
  const face = { hooks: { manager: controller }, request };
@@ -368,7 +393,7 @@ function SkillSection({ t, useManager, request }) {
368
393
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ext-heading-actions", children: [
369
394
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: busy || !state.revision, onClick: () => setForm("source"), children: t("add") }),
370
395
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: busy || sources.length === 0, onClick: () => setForm("skill"), children: t("newSkill") }),
371
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: busy, onClick: () => request("list"), children: t("refresh") })
396
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: busy, onClick: () => request("list", {}, { refreshSlashCatalog: true }), children: t("refresh") })
372
397
  ] })
373
398
  ] }),
374
399
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PageState, { state, t }),
@@ -446,51 +471,70 @@ function SkillSection({ t, useManager, request }) {
446
471
  }
447
472
  function SkillSources({ sources, skills, t, busy, request }) {
448
473
  const [editing, setEditing] = (0, import_react.useState)("");
449
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("details", { className: "dsh-ext-card dsh-skill-source-manager", children: [
450
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("summary", { children: [
451
- t("skillSources"),
452
- " ",
453
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-ext-count", children: sources.length })
454
- ] }),
455
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { children: sources.map((source) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { children: [
456
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-skill-source-title", children: [
457
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `dsh-ext-status ${source.enabled ? "dsh-ext-status-available" : ""}`, "aria-hidden": "true" }),
458
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
459
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("strong", { children: [
460
- source.id,
461
- " ",
462
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-ext-count", children: skills.filter((skill) => skill.sourceId === source.id).length })
463
- ] }),
464
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: source.location }),
465
- source.issue && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { children: t(`issue_${source.issue}`) })
466
- ] })
474
+ const [confirming, setConfirming] = (0, import_react.useState)("");
475
+ const target = sources.find((source) => source.id === confirming);
476
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
477
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("details", { className: "dsh-ext-card dsh-skill-source-manager", children: [
478
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("summary", { children: [
479
+ t("skillSources"),
480
+ " ",
481
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-ext-count", children: sources.length })
467
482
  ] }),
468
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ext-row-actions", children: [
469
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: busy, onClick: () => setEditing(editing === source.id ? "" : source.id), children: t("edit") }),
470
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
471
- "button",
472
- {
473
- type: "button",
474
- role: "switch",
475
- "aria-checked": source.enabled,
476
- disabled: busy,
477
- onClick: () => request("enable", { id: source.id, enabled: !source.enabled }),
478
- children: t(source.enabled ? "disable" : "enable")
483
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { children: sources.map((source) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { children: [
484
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-skill-source-title", children: [
485
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `dsh-ext-status ${source.enabled ? "dsh-ext-status-available" : ""}`, "aria-hidden": "true" }),
486
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
487
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("strong", { children: [
488
+ source.id,
489
+ " ",
490
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-ext-count", children: skills.filter((skill) => skill.sourceId === source.id).length })
491
+ ] }),
492
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: source.location }),
493
+ source.issue && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { children: t(`issue_${source.issue}`) })
494
+ ] })
495
+ ] }),
496
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ext-row-actions", children: [
497
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: busy, onClick: () => setEditing(editing === source.id ? "" : source.id), children: t("edit") }),
498
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
499
+ "button",
500
+ {
501
+ type: "button",
502
+ role: "switch",
503
+ "aria-checked": source.enabled,
504
+ disabled: busy,
505
+ onClick: () => request("enable", { id: source.id, enabled: !source.enabled }, { refreshSlashCatalog: true }),
506
+ children: t(source.enabled ? "disable" : "enable")
507
+ }
508
+ ),
509
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "dsh-ext-danger", type: "button", disabled: busy, onClick: () => setConfirming(source.id), children: t("delete") })
510
+ ] }),
511
+ editing === source.id && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SkillSourceForm, { source, t, busy, onCancel: () => setEditing(""), onSave: async (args) => {
512
+ if (await request("update-skill-source", args)) setEditing("");
513
+ } })
514
+ ] }, source.id)) })
515
+ ] }),
516
+ target && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
517
+ ConfirmDialog,
518
+ {
519
+ title: t("deleteExtensionTitle"),
520
+ message: t("deleteExtensionConfirm").replace("{name}", target.id),
521
+ t,
522
+ busy,
523
+ onCancel: () => setConfirming(""),
524
+ onConfirm: async () => {
525
+ if (await request("delete-extension", { id: target.id }, { refreshSlashCatalog: true })) {
526
+ setEditing("");
527
+ setConfirming("");
479
528
  }
480
- ),
481
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "dsh-ext-danger", type: "button", disabled: busy, onClick: async () => {
482
- if (window.confirm(t("deleteExtensionConfirm")) && await request("delete-extension", { id: source.id })) setEditing("");
483
- }, children: t("delete") })
484
- ] }),
485
- editing === source.id && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SkillSourceForm, { source, t, busy, onCancel: () => setEditing(""), onSave: async (args) => {
486
- if (await request("update-skill-source", args)) setEditing("");
487
- } })
488
- ] }, source.id)) })
529
+ }
530
+ }
531
+ )
489
532
  ] });
490
533
  }
491
534
  function McpSection({ t, useManager, request }) {
492
535
  const state = useManager((value) => value);
493
536
  const [form, setForm] = (0, import_react.useState)("");
537
+ const [confirming, setConfirming] = (0, import_react.useState)("");
494
538
  const busy = state.loading || state.saving;
495
539
  const rows = state.extensions.filter((row) => row.kind === "mcp");
496
540
  useLiveRefresh(request);
@@ -539,16 +583,30 @@ function McpSection({ t, useManager, request }) {
539
583
  children: t(row.enabled ? "disable" : "enable")
540
584
  }
541
585
  ),
542
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "dsh-ext-danger", type: "button", disabled: busy, onClick: async () => {
543
- if (window.confirm(t("deleteExtensionConfirm")) && await request("delete-extension", { id: row.id })) setForm("");
544
- }, children: t("delete") })
586
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "dsh-ext-danger", type: "button", disabled: busy, onClick: () => setConfirming(row.id), children: t("delete") })
545
587
  ] })
546
588
  ] }, row.id)) }),
547
589
  form === "new" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(McpForm, { t, busy, onCancel: () => setForm(""), onSave: async (args) => {
548
590
  if (await request("add-mcp", args)) setForm("");
549
591
  } })
550
592
  ] }),
551
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dsh-ext-footnote", children: t("mcpFootnote") })
593
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dsh-ext-footnote", children: t("mcpFootnote") }),
594
+ confirming && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
595
+ ConfirmDialog,
596
+ {
597
+ title: t("deleteMcpTitle"),
598
+ message: t("deleteMcpConfirm").replace("{name}", confirming),
599
+ t,
600
+ busy,
601
+ onCancel: () => setConfirming(""),
602
+ onConfirm: async () => {
603
+ if (await request("delete-extension", { id: confirming })) {
604
+ setForm("");
605
+ setConfirming("");
606
+ }
607
+ }
608
+ }
609
+ )
552
610
  ] });
553
611
  }
554
612
  function useLiveRefresh(request) {
@@ -582,6 +640,7 @@ function skillState(skill) {
582
640
  }
583
641
  function SkillRow({ skill, source, groups, t, busy, request }) {
584
642
  const [editing, setEditing] = (0, import_react.useState)(false);
643
+ const [confirming, setConfirming] = (0, import_react.useState)(false);
585
644
  const state = skillState(skill);
586
645
  const invocation = skill.modelInvocable && skill.userInvocable ? "both" : skill.modelInvocable ? "modelOnly" : skill.userInvocable ? "manualOnly" : "none";
587
646
  const statusLabel = !skill.valid ? "skillStatusInvalid" : !skill.sourceEnabled ? "skillStatusSourceDisabled" : skill.shadowed ? "skillStatusShadowed" : "";
@@ -639,21 +698,72 @@ function SkillRow({ skill, source, groups, t, busy, request }) {
639
698
  ] }),
640
699
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ext-row-actions", children: [
641
700
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: busy || !skill.valid, onClick: () => setEditing(!editing), children: t("edit") }),
642
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "dsh-ext-danger", type: "button", disabled: busy || !skill.valid, onClick: () => {
643
- if (window.confirm(t("deleteSkillConfirm"))) void request("delete-skill", {
644
- sourceId: skill.sourceId,
645
- relativePath: skill.relativePath,
646
- skillRevision: skill.revision
647
- });
648
- }, children: t("delete") })
701
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "dsh-ext-danger", type: "button", disabled: busy || !skill.valid, onClick: () => setConfirming(true), children: t("delete") })
649
702
  ] })
650
703
  ] })
651
704
  ] }),
652
705
  editing && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SkillEditForm, { skill, t, busy, onCancel: () => setEditing(false), onSave: async (args) => {
653
706
  if (await request("update-skill", args)) setEditing(false);
654
- } })
707
+ } }),
708
+ confirming && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
709
+ ConfirmDialog,
710
+ {
711
+ title: t("deleteSkillTitle"),
712
+ message: t("deleteSkillConfirm").replace("{name}", skill.name),
713
+ t,
714
+ busy,
715
+ onCancel: () => setConfirming(false),
716
+ onConfirm: async () => {
717
+ await request("delete-skill", {
718
+ sourceId: skill.sourceId,
719
+ relativePath: skill.relativePath,
720
+ skillRevision: skill.revision
721
+ });
722
+ }
723
+ }
724
+ )
655
725
  ] }) });
656
726
  }
727
+ function ConfirmDialog({ title, message, t, busy, onConfirm, onCancel }) {
728
+ const dialog = (0, import_react.useRef)(null);
729
+ const cancel = (0, import_react.useRef)(null);
730
+ const titleId = (0, import_react.useId)();
731
+ const messageId = (0, import_react.useId)();
732
+ (0, import_react.useEffect)(() => {
733
+ dialog.current.showModal();
734
+ cancel.current.focus();
735
+ return () => {
736
+ if (dialog.current?.open) dialog.current.close();
737
+ };
738
+ }, []);
739
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
740
+ "dialog",
741
+ {
742
+ ref: dialog,
743
+ className: "dsh-confirm",
744
+ "aria-labelledby": titleId,
745
+ "aria-describedby": messageId,
746
+ onCancel: (event) => {
747
+ event.preventDefault();
748
+ if (!busy) onCancel();
749
+ },
750
+ onMouseDown: (event) => {
751
+ if (event.target === event.currentTarget && !busy) onCancel();
752
+ },
753
+ children: [
754
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-confirm-icon", "aria-hidden": "true", children: "!" }),
755
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-confirm-copy", children: [
756
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { id: titleId, children: title }),
757
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { id: messageId, children: message })
758
+ ] }),
759
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-confirm-actions", children: [
760
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { ref: cancel, type: "button", disabled: busy, onClick: onCancel, children: t("cancel") }),
761
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "dsh-confirm-delete", type: "button", disabled: busy, onClick: () => void onConfirm(), children: t(busy ? "deleting" : "confirmDelete") })
762
+ ] })
763
+ ]
764
+ }
765
+ );
766
+ }
657
767
  function SkillEditForm({ skill, t, busy, onSave, onCancel }) {
658
768
  const prefix = (0, import_react.useId)();
659
769
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("form", { className: "dsh-skill-edit-form", onSubmit: (event) => {
@@ -837,7 +947,8 @@ function McpForm({ row, t, busy, onSave, onCancel }) {
837
947
  t("transport"),
838
948
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", { id: `${prefix}-transport`, value: transport, onChange: (event) => setTransport(event.target.value), children: [
839
949
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "stdio", children: t("stdio") }),
840
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "streamable-http", children: t("http") })
950
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "streamable-http", children: t("http") }),
951
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "sse", children: t("sse") })
841
952
  ] })
842
953
  ] }),
843
954
  transport === "stdio" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
@@ -874,7 +985,8 @@ function AddForm({ kind, t, busy, onSave, onCancel }) {
874
985
  t("transport"),
875
986
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", { id: `${prefix}-transport`, value: transport, onChange: (event) => setTransport(event.target.value), children: [
876
987
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "stdio", children: t("stdio") }),
877
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "streamable-http", children: t("http") })
988
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "streamable-http", children: t("http") }),
989
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "sse", children: t("sse") })
878
990
  ] })
879
991
  ] }),
880
992
  transport === "stdio" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@civilization/deepseek-harness-skill-mcp",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Skill path and MCP server management for DeepSeek Harness Web",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -18,6 +18,7 @@
18
18
  "exports": {
19
19
  ".": "./src/index.js",
20
20
  "./client": "./lib/client.js",
21
+ "./mcp-sse": "./src/mcp-sse.js",
21
22
  "./package.json": "./package.json"
22
23
  },
23
24
  "files": [
@@ -52,6 +53,7 @@
52
53
  "@deepseek-ai/dsh-mcp-client": "0.1.2-rc.1",
53
54
  "@deepseek-ai/dsh-skill-filesystem": "0.1.2-rc.1",
54
55
  "@deepseek-ai/schemastery": "3.18.2",
56
+ "@modelcontextprotocol/sdk": "1.29.0",
55
57
  "yaml": "^2.9.0"
56
58
  },
57
59
  "peerDependencies": {
@@ -63,7 +65,6 @@
63
65
  "@deepseek-ai/dsh-skill": "0.1.2-rc.1",
64
66
  "@deepseek-ai/dsh-system-prompt": "0.1.2-rc.1",
65
67
  "@deepseek-ai/dsh-tools": "0.1.2-rc.1",
66
- "@modelcontextprotocol/sdk": "1.29.0",
67
68
  "esbuild": "^0.28.2",
68
69
  "react": "^19.2.8"
69
70
  }
@@ -1,4 +1,13 @@
1
1
  /** One observable per mounted plugin; drafts stay in the settings component. */
2
+ const skillCatalogMutations = new Set([
3
+ 'add-skill', 'update-skill-source', 'create-skill', 'update-skill', 'delete-skill', 'set-skill-invocation',
4
+ ])
5
+
6
+ /** Keep broad connection cache invalidation away from ordinary page reads and MCP-only writes. */
7
+ export function refreshesSlashCatalog(endpoint, options = {}) {
8
+ return options.refreshSlashCatalog === true || skillCatalogMutations.has(endpoint)
9
+ }
10
+
2
11
  export class ExtensionsController {
3
12
  state = { extensions: [], sources: [], skills: [], revision: '', loading: false, saving: false, error: '', saved: false }
4
13
  listeners = new Set()
@@ -1,6 +1,6 @@
1
1
  /** Skill and MCP settings sections registered through the public Slots and Connection services. */
2
- import { useEffect, useId, useState } from 'react'
3
- import { ExtensionsController } from './controller.js'
2
+ import { useEffect, useId, useRef, useState } from 'react'
3
+ import { ExtensionsController, refreshesSlashCatalog } from './controller.js'
4
4
  import { zh, en } from './locales.js'
5
5
  import css from './styles.css'
6
6
 
@@ -27,7 +27,7 @@ export function apply(ctx) {
27
27
  const t = ctx.locale.bind(namespace)
28
28
  const request = async (endpoint, args, options) => {
29
29
  const ok = await controller.request(endpoint, args, options)
30
- if (ok && (endpoint !== 'list' || options?.silent !== true)) refreshSlashCatalog()
30
+ if (ok && refreshesSlashCatalog(endpoint, options)) refreshSlashCatalog()
31
31
  return ok
32
32
  }
33
33
  const face = { hooks: { manager: controller }, request }
@@ -70,7 +70,7 @@ export function SkillSection({ t, useManager, request }) {
70
70
  <header className="dsh-ext-heading"><div><h2>{t('navSkill')}</h2><p>{t('skillIntro')}</p></div>
71
71
  <div className="dsh-ext-heading-actions"><button type="button" disabled={busy || !state.revision} onClick={() => setForm('source')}>{t('add')}</button>
72
72
  <button type="button" disabled={busy || sources.length === 0} onClick={() => setForm('skill')}>{t('newSkill')}</button>
73
- <button type="button" disabled={busy} onClick={() => request('list')}>{t('refresh')}</button></div></header>
73
+ <button type="button" disabled={busy} onClick={() => request('list', {}, { refreshSlashCatalog: true })}>{t('refresh')}</button></div></header>
74
74
  <PageState state={state} t={t} />
75
75
  <div className="dsh-skill-stats">
76
76
  {Object.entries(counts).map(([key, value]) => <div key={key}><strong>{value}</strong><span>{t(`stat_${key}`)}</span></div>)}
@@ -107,25 +107,29 @@ export function SkillSection({ t, useManager, request }) {
107
107
 
108
108
  function SkillSources({ sources, skills, t, busy, request }) {
109
109
  const [editing, setEditing] = useState('')
110
- return <details className="dsh-ext-card dsh-skill-source-manager"><summary>{t('skillSources')} <span className="dsh-ext-count">{sources.length}</span></summary>
110
+ const [confirming, setConfirming] = useState('')
111
+ const target = sources.find(source => source.id === confirming)
112
+ return <><details className="dsh-ext-card dsh-skill-source-manager"><summary>{t('skillSources')} <span className="dsh-ext-count">{sources.length}</span></summary>
111
113
  <ul>{sources.map(source => <li key={source.id}><div className="dsh-skill-source-title"><span className={`dsh-ext-status ${source.enabled ? 'dsh-ext-status-available' : ''}`} aria-hidden="true" />
112
114
  <div><strong>{source.id} <span className="dsh-ext-count">{skills.filter(skill => skill.sourceId === source.id).length}</span></strong><code>{source.location}</code>
113
115
  {source.issue && <small>{t(`issue_${source.issue}`)}</small>}</div></div>
114
116
  <div className="dsh-ext-row-actions"><button type="button" disabled={busy} onClick={() => setEditing(editing === source.id ? '' : source.id)}>{t('edit')}</button>
115
117
  <button type="button" role="switch" aria-checked={source.enabled} disabled={busy}
116
- onClick={() => request('enable', { id: source.id, enabled: !source.enabled })}>{t(source.enabled ? 'disable' : 'enable')}</button>
117
- <button className="dsh-ext-danger" type="button" disabled={busy} onClick={async () => {
118
- if (window.confirm(t('deleteExtensionConfirm')) && await request('delete-extension', { id: source.id })) setEditing('')
119
- }}>{t('delete')}</button></div>
118
+ onClick={() => request('enable', { id: source.id, enabled: !source.enabled }, { refreshSlashCatalog: true })}>{t(source.enabled ? 'disable' : 'enable')}</button>
119
+ <button className="dsh-ext-danger" type="button" disabled={busy} onClick={() => setConfirming(source.id)}>{t('delete')}</button></div>
120
120
  {editing === source.id && <SkillSourceForm source={source} t={t} busy={busy} onCancel={() => setEditing('')} onSave={async args => {
121
121
  if (await request('update-skill-source', args)) setEditing('')
122
122
  }} />}</li>)}</ul>
123
- </details>
123
+ </details>{target && <ConfirmDialog title={t('deleteExtensionTitle')} message={t('deleteExtensionConfirm').replace('{name}', target.id)}
124
+ t={t} busy={busy} onCancel={() => setConfirming('')} onConfirm={async () => {
125
+ if (await request('delete-extension', { id: target.id }, { refreshSlashCatalog: true })) { setEditing(''); setConfirming('') }
126
+ }} />}</>
124
127
  }
125
128
 
126
129
  export function McpSection({ t, useManager, request }) {
127
130
  const state = useManager(value => value)
128
131
  const [form, setForm] = useState('')
132
+ const [confirming, setConfirming] = useState('')
129
133
  const busy = state.loading || state.saving
130
134
  const rows = state.extensions.filter(row => row.kind === 'mcp')
131
135
  useLiveRefresh(request)
@@ -144,14 +148,16 @@ export function McpSection({ t, useManager, request }) {
144
148
  <div className="dsh-ext-row-actions"><button type="button" disabled={busy} onClick={() => setForm(form === row.id ? '' : row.id)}>{t('edit')}</button>
145
149
  <button type="button" role="switch" aria-checked={row.enabled} aria-label={`${t(row.enabled ? 'disable' : 'enable')} ${row.id}`} disabled={busy}
146
150
  onClick={() => request('enable', { id: row.id, enabled: !row.enabled })}>{t(row.enabled ? 'disable' : 'enable')}</button>
147
- <button className="dsh-ext-danger" type="button" disabled={busy} onClick={async () => {
148
- if (window.confirm(t('deleteExtensionConfirm')) && await request('delete-extension', { id: row.id })) setForm('')
149
- }}>{t('delete')}</button></div></li>)}</ul>
151
+ <button className="dsh-ext-danger" type="button" disabled={busy} onClick={() => setConfirming(row.id)}>{t('delete')}</button></div></li>)}</ul>
150
152
  {form === 'new' && <McpForm t={t} busy={busy} onCancel={() => setForm('')} onSave={async args => {
151
153
  if (await request('add-mcp', args)) setForm('')
152
154
  }} />}
153
155
  </section>
154
156
  <p className="dsh-ext-footnote">{t('mcpFootnote')}</p>
157
+ {confirming && <ConfirmDialog title={t('deleteMcpTitle')} message={t('deleteMcpConfirm').replace('{name}', confirming)}
158
+ t={t} busy={busy} onCancel={() => setConfirming('')} onConfirm={async () => {
159
+ if (await request('delete-extension', { id: confirming })) { setForm(''); setConfirming('') }
160
+ }} />}
155
161
  </section>
156
162
  }
157
163
 
@@ -188,6 +194,7 @@ function skillState(skill) {
188
194
 
189
195
  function SkillRow({ skill, source, groups, t, busy, request }) {
190
196
  const [editing, setEditing] = useState(false)
197
+ const [confirming, setConfirming] = useState(false)
191
198
  const state = skillState(skill)
192
199
  const invocation = skill.modelInvocable && skill.userInvocable ? 'both'
193
200
  : skill.modelInvocable ? 'modelOnly' : skill.userInvocable ? 'manualOnly' : 'none'
@@ -214,18 +221,41 @@ function SkillRow({ skill, source, groups, t, busy, request }) {
214
221
  ? <ul className="dsh-skill-resources">{skill.resources.map(path => <li key={path}><code>{path}</code></li>)}</ul>
215
222
  : <p>{t('noResources')}</p>}</div>
216
223
  <div className="dsh-ext-row-actions"><button type="button" disabled={busy || !skill.valid} onClick={() => setEditing(!editing)}>{t('edit')}</button>
217
- <button className="dsh-ext-danger" type="button" disabled={busy || !skill.valid} onClick={() => {
218
- if (window.confirm(t('deleteSkillConfirm'))) void request('delete-skill', {
219
- sourceId: skill.sourceId, relativePath: skill.relativePath, skillRevision: skill.revision,
220
- })
221
- }}>{t('delete')}</button></div>
224
+ <button className="dsh-ext-danger" type="button" disabled={busy || !skill.valid} onClick={() => setConfirming(true)}>{t('delete')}</button></div>
222
225
  </div></details>
223
226
  {editing && <SkillEditForm skill={skill} t={t} busy={busy} onCancel={() => setEditing(false)} onSave={async args => {
224
227
  if (await request('update-skill', args)) setEditing(false)
225
228
  }} />}
229
+ {confirming && <ConfirmDialog title={t('deleteSkillTitle')} message={t('deleteSkillConfirm').replace('{name}', skill.name)}
230
+ t={t} busy={busy} onCancel={() => setConfirming(false)} onConfirm={async () => {
231
+ await request('delete-skill', {
232
+ sourceId: skill.sourceId, relativePath: skill.relativePath, skillRevision: skill.revision,
233
+ })
234
+ }} />}
226
235
  </div></li>
227
236
  }
228
237
 
238
+ function ConfirmDialog({ title, message, t, busy, onConfirm, onCancel }) {
239
+ const dialog = useRef(null)
240
+ const cancel = useRef(null)
241
+ const titleId = useId()
242
+ const messageId = useId()
243
+ useEffect(() => {
244
+ dialog.current.showModal()
245
+ cancel.current.focus()
246
+ return () => { if (dialog.current?.open) dialog.current.close() }
247
+ }, [])
248
+ return <dialog ref={dialog} className="dsh-confirm" aria-labelledby={titleId} aria-describedby={messageId}
249
+ onCancel={event => { event.preventDefault(); if (!busy) onCancel() }} onMouseDown={event => {
250
+ if (event.target === event.currentTarget && !busy) onCancel()
251
+ }}>
252
+ <div className="dsh-confirm-icon" aria-hidden="true">!</div>
253
+ <div className="dsh-confirm-copy"><h3 id={titleId}>{title}</h3><p id={messageId}>{message}</p></div>
254
+ <div className="dsh-confirm-actions"><button ref={cancel} type="button" disabled={busy} onClick={onCancel}>{t('cancel')}</button>
255
+ <button className="dsh-confirm-delete" type="button" disabled={busy} onClick={() => void onConfirm()}>{t(busy ? 'deleting' : 'confirmDelete')}</button></div>
256
+ </dialog>
257
+ }
258
+
229
259
  function SkillEditForm({ skill, t, busy, onSave, onCancel }) {
230
260
  const prefix = useId()
231
261
  return <form className="dsh-skill-edit-form" onSubmit={event => {
@@ -326,7 +356,7 @@ function McpForm({ row, t, busy, onSave, onCancel }) {
326
356
  void onSave({ id: row?.id ?? data.id, configuration })
327
357
  }}><fieldset disabled={busy}>{!row && field('id')}
328
358
  <label htmlFor={`${prefix}-transport`}>{t('transport')}<select id={`${prefix}-transport`} value={transport} onChange={event => setTransport(event.target.value)}>
329
- <option value="stdio">{t('stdio')}</option><option value="streamable-http">{t('http')}</option></select></label>
359
+ <option value="stdio">{t('stdio')}</option><option value="streamable-http">{t('http')}</option><option value="sse">{t('sse')}</option></select></label>
330
360
  {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)}
331
361
  <p>{t('secret')}</p>
332
362
  <div className="dsh-ext-actions"><button type="button" onClick={onCancel}>{t('cancel')}</button><button type="submit">{t(busy ? 'saving' : row ? 'saveChanges' : 'save')}</button></div>
@@ -350,7 +380,7 @@ function AddForm({ kind, t, busy, onSave, onCancel }) {
350
380
  void onSave(args)
351
381
  }}><fieldset disabled={busy}>{kind === 'skill' ? field('directory') : <>{field('id')}
352
382
  <label htmlFor={`${prefix}-transport`}>{t('transport')}<select id={`${prefix}-transport`} value={transport} onChange={event => setTransport(event.target.value)}>
353
- <option value="stdio">{t('stdio')}</option><option value="streamable-http">{t('http')}</option></select></label>
383
+ <option value="stdio">{t('stdio')}</option><option value="streamable-http">{t('http')}</option><option value="sse">{t('sse')}</option></select></label>
354
384
  {transport === 'stdio' ? <>{field('command')}{field('args', false, true)}{field('cwd', false)}</> : field('url')}
355
385
  <p>{t('secret')}</p>
356
386
  </>}
@@ -5,15 +5,17 @@ export const zh = {
5
5
  skills: 'Skill 路径', mcp: 'MCP 服务器', refresh: '刷新', add: '添加路径', addMcp: '添加服务器', newSkill: '新建 Skill',
6
6
  emptySkills: '还没有配置 Skill 路径', emptySkillsHint: '指定一个本地 skills 目录,目录下的各个子目录就是技能。',
7
7
  emptyMcp: '还没有添加 MCP 服务器', emptyMcpHint: '通过本地命令或 HTTP 地址连接外部工具。',
8
- id: '标识', directory: 'Skill 路径', transport: '配置类型', stdio: 'stdio', http: 'HTTP',
8
+ id: '标识', directory: 'Skill 路径', transport: '配置类型', stdio: 'stdio', http: 'Streamable HTTP', sse: 'SSE(旧版兼容)',
9
9
  command: '命令', args: '参数(每行一个)', cwd: '工作目录(可选)', url: '服务器地址',
10
- cancel: '取消', save: '添加', saveChanges: '保存修改', create: '创建', edit: '编辑', delete: '删除', saving: '保存中…', loading: '加载中…',
11
- deleteExtensionConfirm: '确定删除这个配置吗?Skill 来源中的文件不会被删除。', deleteSkillConfirm: '确定永久删除这个 Skill 及其目录内容吗?此操作无法撤销。',
10
+ cancel: '取消', save: '添加', saveChanges: '保存修改', create: '创建', edit: '编辑', delete: '删除', confirmDelete: '确认删除', deleting: '删除中…', saving: '保存中…', loading: '加载中…',
11
+ deleteExtensionTitle: '移除 Skill 来源', deleteMcpTitle: '删除 MCP 服务器', deleteSkillTitle: '永久删除 Skill',
12
+ deleteExtensionConfirm: '将从当前配置中移除“{name}”,目录中的 Skill 文件会保留。', deleteMcpConfirm: '将删除 MCP 服务器“{name}”及其连接配置。',
13
+ deleteSkillConfirm: '将永久删除“{name}”及其目录内的资源文件。此操作无法撤销。',
12
14
  enabled: '配置已启用', disabled: '已禁用', enable: '启用', disable: '禁用',
13
15
  saved: '更改已保存。', failed: '操作失败,请检查输入、连接或服务端日志后重试。',
14
16
  conflict: '配置已被修改,请刷新后重试。未提交的表单内容已保留。',
15
17
  skillFootnote: '新路径默认禁用。启用后,Skill provider 会扫描该路径下的所有技能。配置变更会自动生效。',
16
- mcpFootnote: '新服务器默认禁用。启用后会执行所填命令或连接所填地址。配置变更会自动生效。',
18
+ mcpFootnote: '新服务器默认禁用。新服务推荐 Streamable HTTP;SSE 仅用于兼容旧版 /sse + /messages 服务。配置变更会自动生效。',
17
19
  secret: '请勿填写密钥或带令牌的地址。此版本暂不支持认证参数。',
18
20
  tools: '工具列表', status_available: '工具可用', status_waiting: '等待发现工具', status_disabled: '已禁用',
19
21
  skillHint: '启用后由 Skill provider 扫描;每个配置项对应一个包含多个技能的目录。',
@@ -41,15 +43,17 @@ export const en = {
41
43
  skills: 'Skill paths', mcp: 'MCP servers', refresh: 'Refresh', add: 'Add path', addMcp: 'Add server', newSkill: 'New Skill',
42
44
  emptySkills: 'No Skill paths configured', emptySkillsHint: 'Choose a local skills directory whose children contain the individual skills.',
43
45
  emptyMcp: 'No MCP servers yet', emptyMcpHint: 'Connect external tools with a local command or HTTP endpoint.',
44
- id: 'Identifier', directory: 'Skill path', transport: 'Type', stdio: 'stdio', http: 'HTTP',
46
+ id: 'Identifier', directory: 'Skill path', transport: 'Type', stdio: 'stdio', http: 'Streamable HTTP', sse: 'SSE (legacy)',
45
47
  command: 'Command', args: 'Arguments (one per line)', cwd: 'Working directory (optional)', url: 'Server URL',
46
- cancel: 'Cancel', save: 'Add', saveChanges: 'Save changes', create: 'Create', edit: 'Edit', delete: 'Delete', saving: 'Saving…', loading: 'Loading…',
47
- deleteExtensionConfirm: 'Delete this configuration? Files inside a Skill source will not be deleted.', deleteSkillConfirm: 'Permanently delete this Skill and its directory contents? This cannot be undone.',
48
+ cancel: 'Cancel', save: 'Add', saveChanges: 'Save changes', create: 'Create', edit: 'Edit', delete: 'Delete', confirmDelete: 'Delete', deleting: 'Deleting…', saving: 'Saving…', loading: 'Loading…',
49
+ deleteExtensionTitle: 'Remove Skill source', deleteMcpTitle: 'Delete MCP server', deleteSkillTitle: 'Permanently delete Skill',
50
+ deleteExtensionConfirm: 'Remove “{name}” from this configuration. Skill files in the directory will be kept.', deleteMcpConfirm: 'Delete MCP server “{name}” and its connection configuration.',
51
+ deleteSkillConfirm: 'Permanently delete “{name}” and the resource files in its directory. This cannot be undone.',
48
52
  enabled: 'Configured enabled', disabled: 'Disabled', enable: 'Enable', disable: 'Disable',
49
53
  saved: 'Changes saved.', failed: 'Operation failed. Check input, connection or server logs and retry.',
50
54
  conflict: 'Configuration changed. Refresh and retry; your draft has been retained.',
51
55
  skillFootnote: 'New paths are disabled. Once enabled, the Skill provider scans every skill below them.',
52
- mcpFootnote: 'New servers are disabled. Enabling one runs its command or connects to its endpoint; changes apply automatically.',
56
+ mcpFootnote: 'New servers are disabled. Prefer Streamable HTTP for new servers; SSE only supports legacy /sse + /messages endpoints.',
53
57
  secret: 'Do not enter secrets or token-bearing URLs. Authentication fields are not supported yet.',
54
58
  tools: 'Tools', status_available: 'Tools available', status_waiting: 'Waiting for tools', status_disabled: 'Disabled',
55
59
  skillHint: 'The Skill provider scans enabled paths; every configured path contains multiple skills.',
@@ -19,6 +19,8 @@
19
19
  .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}
20
20
  .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}
21
21
  .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)}
22
+ .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)}
23
+ .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}
22
24
  .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}
23
25
  .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)}
24
26
  .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)}
package/src/index.js CHANGED
@@ -35,7 +35,7 @@ export async function apply(ctx, config) {
35
35
  register('extensions_add_skill', 'Add an existing root containing skill directories. Added sources are disabled until explicitly enabled.', {
36
36
  id: string, directory: string,
37
37
  }, (args, exec) => store.addSkill(args.id, args.directory, exec.signal))
38
- register('extensions_add_mcp', 'Add a disabled MCP server. configuration is JSON with transport and command/args/cwd or url. Do not supply secrets in arguments.', {
38
+ register('extensions_add_mcp', 'Add a disabled MCP server. configuration is JSON for stdio, Streamable HTTP, or legacy SSE transport. Do not supply secrets in arguments.', {
39
39
  id: string, configuration: string,
40
40
  }, (args, exec) => {
41
41
  const input = JSON.parse(args.configuration)
package/src/mcp-sse.js ADDED
@@ -0,0 +1,122 @@
1
+ /** Legacy HTTP+SSE MCP client used only for servers that predate Streamable HTTP. */
2
+ import Schema from '@deepseek-ai/schemastery'
3
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js'
4
+ import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
5
+ import { CallToolResultSchema, ListToolsResultSchema, ToolListChangedNotificationSchema } from '@modelcontextprotocol/sdk/types.js'
6
+ import { createHash } from 'node:crypto'
7
+
8
+ export const name = 'mcp-sse-client'
9
+ export const inject = ['tools']
10
+
11
+ const serverNamePattern = /^[A-Za-z0-9_-]{1,32}$/
12
+ const invalidNameCharacters = /[^A-Za-z0-9_-]/g
13
+ const maxPublicNameLength = 64
14
+
15
+ export const Config = Schema.object({
16
+ transport: Schema.const('sse'),
17
+ serverName: Schema.string().required().pattern(serverNamePattern),
18
+ url: Schema.string().required(),
19
+ toolCallTimeoutMs: Schema.number().default(60_000),
20
+ failOnStartupError: Schema.boolean().default(false),
21
+ })
22
+
23
+ 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))
26
+ let disposed = false
27
+ let registrations = new Map()
28
+ let synchronization = Promise.resolve()
29
+
30
+ const clearTools = () => {
31
+ for (const dispose of registrations.values()) dispose()
32
+ registrations = new Map()
33
+ }
34
+ ctx.effect(() => () => {
35
+ disposed = true
36
+ clearTools()
37
+ void client.close().catch(() => {})
38
+ }, 'mcp-sse-client.connection')
39
+
40
+ const synchronizeTools = async () => {
41
+ const definitions = new Map()
42
+ let cursor
43
+ do {
44
+ const response = await client.request({
45
+ method: 'tools/list',
46
+ ...(cursor === undefined ? {} : { params: { cursor } }),
47
+ }, ListToolsResultSchema)
48
+ for (const tool of response.tools) {
49
+ const publicName = publicToolName(config.serverName, tool.name)
50
+ if (definitions.has(publicName)) throw new Error(`SSE MCP server listed duplicate tool: ${tool.name}`)
51
+ definitions.set(publicName, createTool(client, publicName, tool, config.toolCallTimeoutMs))
52
+ }
53
+ cursor = response.nextCursor
54
+ } while (cursor)
55
+ if (disposed) return
56
+ clearTools()
57
+ const next = new Map()
58
+ try {
59
+ for (const [publicName, definition] of definitions) next.set(publicName, ctx.tools.register(definition))
60
+ registrations = next
61
+ } catch (error) {
62
+ for (const dispose of next.values()) dispose()
63
+ throw error
64
+ }
65
+ }
66
+
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)}`)
70
+ })
71
+ })
72
+
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)}`)
81
+ }
82
+ }
83
+
84
+ function createTool(client, publicName, tool, timeout) {
85
+ return {
86
+ name: publicName,
87
+ description: tool.description ?? '',
88
+ parameters: tool.inputSchema,
89
+ output: {
90
+ schema: {
91
+ type: 'object',
92
+ properties: { content: { type: 'array', items: {} }, structuredContent: {} },
93
+ required: ['content'],
94
+ additionalProperties: true,
95
+ },
96
+ render: (_args, value) => [{ type: 'text', text: textContent(value.content) }],
97
+ },
98
+ async execute(args, exec) {
99
+ const result = await client.request({ method: 'tools/call', params: { name: tool.name, arguments: args } }, CallToolResultSchema, {
100
+ signal: exec.signal,
101
+ timeout,
102
+ })
103
+ if (result.isError) throw new Error(textContent(result.content) || `MCP tool ${tool.name} failed`)
104
+ return result
105
+ },
106
+ }
107
+ }
108
+
109
+ function publicToolName(serverName, rawName) {
110
+ const joined = `mcp__${serverName}__${rawName}`
111
+ const normalized = joined.replace(invalidNameCharacters, '_')
112
+ if (normalized === joined && normalized.length <= maxPublicNameLength) return normalized
113
+ const hash = createHash('sha256').update(`${serverName}\0${rawName}`).digest('hex').slice(0, 12)
114
+ return `${normalized.slice(0, maxPublicNameLength - 13)}_${hash}`
115
+ }
116
+
117
+ function textContent(content) {
118
+ const text = Array.isArray(content) ? content.filter(item => item?.type === 'text').map(item => item.text).join('\n') : ''
119
+ return text || JSON.stringify(content ?? [])
120
+ }
121
+
122
+ const messageOf = error => error instanceof Error ? error.message : String(error)
package/src/store.js CHANGED
@@ -4,11 +4,13 @@ import { basename, isAbsolute, resolve, win32 } from 'node:path'
4
4
  import { randomUUID, createHash } from 'node:crypto'
5
5
  import { Config as McpConfig } from '@deepseek-ai/dsh-mcp-client'
6
6
  import { Config as SkillConfig } from '@deepseek-ai/dsh-skill-filesystem'
7
+ import { Config as SseMcpConfig } from './mcp-sse.js'
7
8
  import { parse, stringify } from 'yaml'
8
9
 
9
10
  const moduleNames = {
10
11
  skill: '@deepseek-ai/dsh-skill-filesystem',
11
12
  mcp: '@deepseek-ai/dsh-mcp-client',
13
+ mcpSse: '@civilization/deepseek-harness-skill-mcp/mcp-sse',
12
14
  }
13
15
  const managerModule = new URL('./index.js', import.meta.url).href
14
16
  const resolvedModules = Object.fromEntries(Object.entries(moduleNames).map(([kind, name]) => [kind, import.meta.resolve(name)]))
@@ -61,6 +63,9 @@ function parsePatch(value) {
61
63
  } else if (isModule(row.name, 'mcp')) {
62
64
  McpConfig(row.config)
63
65
  if (row.config.serverName !== row.id) throw new Error('MCP namespace must match its id.')
66
+ } else if (isModule(row.name, 'mcpSse')) {
67
+ SseMcpConfig(row.config)
68
+ if (row.config.serverName !== row.id) throw new Error('MCP namespace must match its id.')
64
69
  } else throw new Error('Unknown managed plugin module.')
65
70
  managed.push(row)
66
71
  }
@@ -183,7 +188,8 @@ export class ExtensionStore {
183
188
  async addMcp(id, input, signal, expectedRevision) {
184
189
  validateId(id)
185
190
  const config = this.normalizeMcp(id, input)
186
- return this.add({ id, name: moduleNames.mcp, disabled: true, config }, signal, expectedRevision)
191
+ const name = config.transport === 'sse' ? moduleNames.mcpSse : moduleNames.mcp
192
+ return this.add({ id, name, disabled: true, config }, signal, expectedRevision)
187
193
  }
188
194
 
189
195
  normalizeMcp(id, input) {
@@ -194,8 +200,10 @@ export class ExtensionStore {
194
200
  command: typeof input.command === 'string' ? normalizeWindowsPath(input.command) : input.command,
195
201
  cwd: typeof input.cwd === 'string' && input.cwd ? normalizeWindowsPath(input.cwd) : input.cwd,
196
202
  } : input
197
- const config = McpConfig({ ...normalized, serverName: id, failOnStartupError: false })
198
- if (config.transport === 'streamable-http') {
203
+ const config = input.transport === 'sse'
204
+ ? SseMcpConfig({ ...normalized, serverName: id, failOnStartupError: false })
205
+ : McpConfig({ ...normalized, serverName: id, failOnStartupError: false })
206
+ if (config.transport === 'streamable-http' || config.transport === 'sse') {
199
207
  const url = new URL(config.url)
200
208
  if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.search || url.hash) throw new Error('Use an HTTP(S) endpoint without credentials, query or fragment.')
201
209
  } else if (!config.command.trim()) throw new Error('MCP command cannot be empty.')
@@ -235,6 +243,7 @@ export class ExtensionStore {
235
243
  return this.update(rows => {
236
244
  const row = rows.find(row => row.id === id && row.config.serverName)
237
245
  if (!row) throw new Error('Unknown MCP server.')
246
+ row.name = config.transport === 'sse' ? moduleNames.mcpSse : moduleNames.mcp
238
247
  row.config = config
239
248
  }, signal, expectedRevision)
240
249
  }
package/src/web.js CHANGED
@@ -43,7 +43,7 @@ export async function describeExtensions(store, ctx) {
43
43
  kind: row.config.serverName ? 'mcp' : 'skill',
44
44
  enabled: !row.disabled,
45
45
  location: row.config.transport === 'stdio' ? row.config.command
46
- : row.config.transport === 'streamable-http' ? new URL(row.config.url).origin
46
+ : ['streamable-http', 'sse'].includes(row.config.transport) ? new URL(row.config.url).origin
47
47
  : row.config.customSkillDirs[0],
48
48
  transport: row.config.transport ?? '',
49
49
  configuration: row.config.serverName ? (row.config.transport === 'stdio'