@civilization/deepseek-harness-skill-mcp 0.1.6 → 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
@@ -110,7 +110,8 @@ var zh = {
110
110
  directory: "Skill \u8DEF\u5F84",
111
111
  transport: "\u914D\u7F6E\u7C7B\u578B",
112
112
  stdio: "stdio",
113
- http: "HTTP",
113
+ http: "Streamable HTTP",
114
+ sse: "SSE\uFF08\u65E7\u7248\u517C\u5BB9\uFF09",
114
115
  command: "\u547D\u4EE4",
115
116
  args: "\u53C2\u6570\uFF08\u6BCF\u884C\u4E00\u4E2A\uFF09",
116
117
  cwd: "\u5DE5\u4F5C\u76EE\u5F55\uFF08\u53EF\u9009\uFF09",
@@ -139,7 +140,7 @@ var zh = {
139
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",
140
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",
141
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",
142
- 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",
143
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",
144
145
  tools: "\u5DE5\u5177\u5217\u8868",
145
146
  status_available: "\u5DE5\u5177\u53EF\u7528",
@@ -220,7 +221,8 @@ var en = {
220
221
  directory: "Skill path",
221
222
  transport: "Type",
222
223
  stdio: "stdio",
223
- http: "HTTP",
224
+ http: "Streamable HTTP",
225
+ sse: "SSE (legacy)",
224
226
  command: "Command",
225
227
  args: "Arguments (one per line)",
226
228
  cwd: "Working directory (optional)",
@@ -249,7 +251,7 @@ var en = {
249
251
  failed: "Operation failed. Check input, connection or server logs and retry.",
250
252
  conflict: "Configuration changed. Refresh and retry; your draft has been retained.",
251
253
  skillFootnote: "New paths are disabled. Once enabled, the Skill provider scans every skill below them.",
252
- 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.",
253
255
  secret: "Do not enter secrets or token-bearing URLs. Authentication fields are not supported yet.",
254
256
  tools: "Tools",
255
257
  status_available: "Tools available",
@@ -945,7 +947,8 @@ function McpForm({ row, t, busy, onSave, onCancel }) {
945
947
  t("transport"),
946
948
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", { id: `${prefix}-transport`, value: transport, onChange: (event) => setTransport(event.target.value), children: [
947
949
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "stdio", children: t("stdio") }),
948
- /* @__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") })
949
952
  ] })
950
953
  ] }),
951
954
  transport === "stdio" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
@@ -982,7 +985,8 @@ function AddForm({ kind, t, busy, onSave, onCancel }) {
982
985
  t("transport"),
983
986
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", { id: `${prefix}-transport`, value: transport, onChange: (event) => setTransport(event.target.value), children: [
984
987
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "stdio", children: t("stdio") }),
985
- /* @__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") })
986
990
  ] })
987
991
  ] }),
988
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.6",
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
  }
@@ -356,7 +356,7 @@ function McpForm({ row, t, busy, onSave, onCancel }) {
356
356
  void onSave({ id: row?.id ?? data.id, configuration })
357
357
  }}><fieldset disabled={busy}>{!row && field('id')}
358
358
  <label htmlFor={`${prefix}-transport`}>{t('transport')}<select id={`${prefix}-transport`} value={transport} onChange={event => setTransport(event.target.value)}>
359
- <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>
360
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)}
361
361
  <p>{t('secret')}</p>
362
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>
@@ -380,7 +380,7 @@ function AddForm({ kind, t, busy, onSave, onCancel }) {
380
380
  void onSave(args)
381
381
  }}><fieldset disabled={busy}>{kind === 'skill' ? field('directory') : <>{field('id')}
382
382
  <label htmlFor={`${prefix}-transport`}>{t('transport')}<select id={`${prefix}-transport`} value={transport} onChange={event => setTransport(event.target.value)}>
383
- <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>
384
384
  {transport === 'stdio' ? <>{field('command')}{field('args', false, true)}{field('cwd', false)}</> : field('url')}
385
385
  <p>{t('secret')}</p>
386
386
  </>}
@@ -5,7 +5,7 @@ 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
10
  cancel: '取消', save: '添加', saveChanges: '保存修改', create: '创建', edit: '编辑', delete: '删除', confirmDelete: '确认删除', deleting: '删除中…', saving: '保存中…', loading: '加载中…',
11
11
  deleteExtensionTitle: '移除 Skill 来源', deleteMcpTitle: '删除 MCP 服务器', deleteSkillTitle: '永久删除 Skill',
@@ -15,7 +15,7 @@ export const zh = {
15
15
  saved: '更改已保存。', failed: '操作失败,请检查输入、连接或服务端日志后重试。',
16
16
  conflict: '配置已被修改,请刷新后重试。未提交的表单内容已保留。',
17
17
  skillFootnote: '新路径默认禁用。启用后,Skill provider 会扫描该路径下的所有技能。配置变更会自动生效。',
18
- mcpFootnote: '新服务器默认禁用。启用后会执行所填命令或连接所填地址。配置变更会自动生效。',
18
+ mcpFootnote: '新服务器默认禁用。新服务推荐 Streamable HTTP;SSE 仅用于兼容旧版 /sse + /messages 服务。配置变更会自动生效。',
19
19
  secret: '请勿填写密钥或带令牌的地址。此版本暂不支持认证参数。',
20
20
  tools: '工具列表', status_available: '工具可用', status_waiting: '等待发现工具', status_disabled: '已禁用',
21
21
  skillHint: '启用后由 Skill provider 扫描;每个配置项对应一个包含多个技能的目录。',
@@ -43,7 +43,7 @@ export const en = {
43
43
  skills: 'Skill paths', mcp: 'MCP servers', refresh: 'Refresh', add: 'Add path', addMcp: 'Add server', newSkill: 'New Skill',
44
44
  emptySkills: 'No Skill paths configured', emptySkillsHint: 'Choose a local skills directory whose children contain the individual skills.',
45
45
  emptyMcp: 'No MCP servers yet', emptyMcpHint: 'Connect external tools with a local command or HTTP endpoint.',
46
- 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)',
47
47
  command: 'Command', args: 'Arguments (one per line)', cwd: 'Working directory (optional)', url: 'Server URL',
48
48
  cancel: 'Cancel', save: 'Add', saveChanges: 'Save changes', create: 'Create', edit: 'Edit', delete: 'Delete', confirmDelete: 'Delete', deleting: 'Deleting…', saving: 'Saving…', loading: 'Loading…',
49
49
  deleteExtensionTitle: 'Remove Skill source', deleteMcpTitle: 'Delete MCP server', deleteSkillTitle: 'Permanently delete Skill',
@@ -53,7 +53,7 @@ export const en = {
53
53
  saved: 'Changes saved.', failed: 'Operation failed. Check input, connection or server logs and retry.',
54
54
  conflict: 'Configuration changed. Refresh and retry; your draft has been retained.',
55
55
  skillFootnote: 'New paths are disabled. Once enabled, the Skill provider scans every skill below them.',
56
- 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.',
57
57
  secret: 'Do not enter secrets or token-bearing URLs. Authentication fields are not supported yet.',
58
58
  tools: 'Tools', status_available: 'Tools available', status_waiting: 'Waiting for tools', status_disabled: 'Disabled',
59
59
  skillHint: 'The Skill provider scans enabled paths; every configured path contains multiple skills.',
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'