dsh-plugin-capabilities 0.2.0 → 0.3.0
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 +5 -1
- package/lib/client.js +103 -15
- package/lib/client.js.map +3 -3
- package/lib/index.js +15 -3
- package/lib/index.js.map +4 -4
- package/market/mcp.json +24 -12
- package/market/skills.json +6 -3
- package/package.json +1 -1
- package/src/client/MarketTab.tsx +107 -9
- package/src/client/css.ts +6 -1
- package/src/client/index.ts +2 -0
- package/src/client/locales.ts +8 -0
- package/src/market.test.ts +12 -3
- package/src/market.ts +6 -0
- package/src/opener.ts +9 -1
- package/src/routes.ts +4 -0
- package/src/smoke.test.ts +4 -3
package/market/mcp.json
CHANGED
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"args": ["-y", "@modelcontextprotocol/server-filesystem", "."],
|
|
14
14
|
"homepage": "https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem",
|
|
15
15
|
"category": "files",
|
|
16
|
-
"runtime": "npx (Node.js)"
|
|
16
|
+
"runtime": "npx (Node.js)",
|
|
17
|
+
"tools": ["read_file", "read_multiple_files", "write_file", "edit_file", "create_directory", "list_directory", "directory_tree", "move_file", "search_files", "get_file_info", "list_allowed_directories"]
|
|
17
18
|
},
|
|
18
19
|
{
|
|
19
20
|
"id": "memory",
|
|
@@ -26,7 +27,8 @@
|
|
|
26
27
|
"args": ["-y", "@modelcontextprotocol/server-memory"],
|
|
27
28
|
"homepage": "https://github.com/modelcontextprotocol/servers/tree/main/src/memory",
|
|
28
29
|
"category": "knowledge",
|
|
29
|
-
"runtime": "npx (Node.js)"
|
|
30
|
+
"runtime": "npx (Node.js)",
|
|
31
|
+
"tools": ["create_entities", "create_relations", "add_observations", "delete_entities", "delete_observations", "delete_relations", "read_graph", "search_nodes", "open_nodes"]
|
|
30
32
|
},
|
|
31
33
|
{
|
|
32
34
|
"id": "sequentialthinking",
|
|
@@ -39,7 +41,8 @@
|
|
|
39
41
|
"args": ["-y", "@modelcontextprotocol/server-sequentialthinking"],
|
|
40
42
|
"homepage": "https://github.com/modelcontextprotocol/servers/tree/main/src/sequentialthinking",
|
|
41
43
|
"category": "reasoning",
|
|
42
|
-
"runtime": "npx (Node.js)"
|
|
44
|
+
"runtime": "npx (Node.js)",
|
|
45
|
+
"tools": ["sequentialthinking"]
|
|
43
46
|
},
|
|
44
47
|
{
|
|
45
48
|
"id": "everything",
|
|
@@ -52,7 +55,8 @@
|
|
|
52
55
|
"args": ["-y", "@modelcontextprotocol/server-everything"],
|
|
53
56
|
"homepage": "https://github.com/modelcontextprotocol/servers/tree/main/src/everything",
|
|
54
57
|
"category": "test",
|
|
55
|
-
"runtime": "npx (Node.js)"
|
|
58
|
+
"runtime": "npx (Node.js)",
|
|
59
|
+
"tools": ["echo", "add", "sampleLLM", "longRunningOperation", "getTinyImage", "annotatedMessage"]
|
|
56
60
|
},
|
|
57
61
|
{
|
|
58
62
|
"id": "git",
|
|
@@ -65,7 +69,8 @@
|
|
|
65
69
|
"args": ["mcp-server-git"],
|
|
66
70
|
"homepage": "https://github.com/modelcontextprotocol/servers/tree/main/src/git",
|
|
67
71
|
"category": "dev",
|
|
68
|
-
"runtime": "uvx (Python/uv)"
|
|
72
|
+
"runtime": "uvx (Python/uv)",
|
|
73
|
+
"tools": ["git_status", "git_diff_unstaged", "git_diff_staged", "git_diff", "git_add", "git_commit", "git_reset", "git_log", "git_create_branch", "git_checkout_branch", "git_list_branches", "git_create_tag", "git_delete_tag", "git_list_tags", "git_push", "git_pull"]
|
|
69
74
|
},
|
|
70
75
|
{
|
|
71
76
|
"id": "fetch",
|
|
@@ -78,7 +83,8 @@
|
|
|
78
83
|
"args": ["mcp-server-fetch"],
|
|
79
84
|
"homepage": "https://github.com/modelcontextprotocol/servers/tree/main/src/fetch",
|
|
80
85
|
"category": "web",
|
|
81
|
-
"runtime": "uvx (Python/uv)"
|
|
86
|
+
"runtime": "uvx (Python/uv)",
|
|
87
|
+
"tools": ["fetch"]
|
|
82
88
|
},
|
|
83
89
|
{
|
|
84
90
|
"id": "sqlite",
|
|
@@ -91,7 +97,8 @@
|
|
|
91
97
|
"args": ["mcp-server-sqlite", "--db-path", "sqlite.db"],
|
|
92
98
|
"homepage": "https://github.com/modelcontextprotocol/servers/tree/main/src/sqlite",
|
|
93
99
|
"category": "data",
|
|
94
|
-
"runtime": "uvx (Python/uv)"
|
|
100
|
+
"runtime": "uvx (Python/uv)",
|
|
101
|
+
"tools": ["read_query", "write_query", "create_table", "list_tables", "describe_table", "append_insight"]
|
|
95
102
|
},
|
|
96
103
|
{
|
|
97
104
|
"id": "time",
|
|
@@ -104,7 +111,8 @@
|
|
|
104
111
|
"args": ["mcp-server-time"],
|
|
105
112
|
"homepage": "https://github.com/modelcontextprotocol/servers/tree/main/src/time",
|
|
106
113
|
"category": "knowledge",
|
|
107
|
-
"runtime": "uvx (Python/uv)"
|
|
114
|
+
"runtime": "uvx (Python/uv)",
|
|
115
|
+
"tools": ["get_current_time", "convert_time"]
|
|
108
116
|
},
|
|
109
117
|
{
|
|
110
118
|
"id": "github",
|
|
@@ -118,7 +126,8 @@
|
|
|
118
126
|
"envKeys": ["GITHUB_PERSONAL_ACCESS_TOKEN"],
|
|
119
127
|
"homepage": "https://github.com/modelcontextprotocol/servers/tree/main/src/github",
|
|
120
128
|
"category": "dev",
|
|
121
|
-
"runtime": "npx (Node.js)"
|
|
129
|
+
"runtime": "npx (Node.js)",
|
|
130
|
+
"tools": ["create_or_update_file", "create_branch", "create_issue", "add_issue_comment", "create_pull_request", "create_pull_request_review", "fork_repository", "push_files", "get_file_contents", "get_issue", "get_pull_request", "get_pull_request_files", "get_pull_request_status", "get_pull_request_comments", "list_commits", "list_issues", "list_pull_requests", "list_branches", "merge_pull_request", "search_code", "search_issues", "search_repositories", "search_users", "update_issue", "update_pull_request_branch"]
|
|
122
131
|
},
|
|
123
132
|
{
|
|
124
133
|
"id": "brave-search",
|
|
@@ -132,7 +141,8 @@
|
|
|
132
141
|
"envKeys": ["BRAVE_API_KEY"],
|
|
133
142
|
"homepage": "https://github.com/modelcontextprotocol/servers/tree/main/src/brave-search",
|
|
134
143
|
"category": "web",
|
|
135
|
-
"runtime": "npx (Node.js)"
|
|
144
|
+
"runtime": "npx (Node.js)",
|
|
145
|
+
"tools": ["brave_web_search", "brave_local_search"]
|
|
136
146
|
},
|
|
137
147
|
{
|
|
138
148
|
"id": "puppeteer",
|
|
@@ -145,7 +155,8 @@
|
|
|
145
155
|
"args": ["-y", "@modelcontextprotocol/server-puppeteer"],
|
|
146
156
|
"homepage": "https://github.com/modelcontextprotocol/servers/tree/main/src/puppeteer",
|
|
147
157
|
"category": "web",
|
|
148
|
-
"runtime": "npx (Node.js)"
|
|
158
|
+
"runtime": "npx (Node.js)",
|
|
159
|
+
"tools": ["puppeteer_navigate", "puppeteer_screenshot", "puppeteer_click", "puppeteer_fill", "puppeteer_evaluate", "puppeteer_select"]
|
|
149
160
|
},
|
|
150
161
|
{
|
|
151
162
|
"id": "context7",
|
|
@@ -158,7 +169,8 @@
|
|
|
158
169
|
"args": ["-y", "@upstash/context7-mcp"],
|
|
159
170
|
"homepage": "https://github.com/upstash/context7",
|
|
160
171
|
"category": "knowledge",
|
|
161
|
-
"runtime": "npx (Node.js)"
|
|
172
|
+
"runtime": "npx (Node.js)",
|
|
173
|
+
"tools": ["resolve-library-id", "get-library-docs"]
|
|
162
174
|
}
|
|
163
175
|
]
|
|
164
176
|
}
|
package/market/skills.json
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"descriptionZh": "Anthropic 官方技能仓库:文档技能(docx/pdf/pptx/xlsx)、artifacts-builder、canvas-design、mcp-builder 等。",
|
|
11
11
|
"url": "https://github.com/anthropics/skills",
|
|
12
12
|
"homepage": "https://github.com/anthropics/skills",
|
|
13
|
-
"skillCount":
|
|
13
|
+
"skillCount": 19,
|
|
14
|
+
"skills": ["algorithmic-art", "brand-guidelines", "canvas-design", "claude-academy-guide", "claude-api", "discernment-nudge", "doc-coauthoring", "docx", "frontend-design", "internal-comms", "mcp-builder", "pdf", "pptx", "skill-creator", "slack-gif-creator", "theme-factory", "web-artifacts-builder", "webapp-testing", "xlsx"]
|
|
14
15
|
},
|
|
15
16
|
{
|
|
16
17
|
"id": "obra-superpowers",
|
|
@@ -20,7 +21,8 @@
|
|
|
20
21
|
"descriptionZh": "Jesse Vincent 维护的工作流技能集:头脑风暴、系统化调试、测试驱动开发、撰写计划及配套的调度/评审流程。",
|
|
21
22
|
"url": "https://github.com/obra/superpowers",
|
|
22
23
|
"homepage": "https://github.com/obra/superpowers",
|
|
23
|
-
"skillCount":
|
|
24
|
+
"skillCount": 14,
|
|
25
|
+
"skills": ["brainstorming", "dispatching-parallel-agents", "executing-plans", "finishing-a-development-branch", "receiving-code-review", "requesting-code-review", "subagent-driven-development", "systematic-debugging", "test-driven-development", "using-git-worktrees", "using-superpowers", "verification-before-completion", "writing-plans", "writing-skills"]
|
|
24
26
|
},
|
|
25
27
|
{
|
|
26
28
|
"id": "vercel-labs-skills",
|
|
@@ -30,7 +32,8 @@
|
|
|
30
32
|
"descriptionZh": "Vercel Labs 的实用小技能集,包含 find-skills(发现并安装社区技能)。",
|
|
31
33
|
"url": "https://github.com/vercel-labs/skills",
|
|
32
34
|
"homepage": "https://github.com/vercel-labs/skills",
|
|
33
|
-
"skillCount":
|
|
35
|
+
"skillCount": 1,
|
|
36
|
+
"skills": ["find-skills"]
|
|
34
37
|
}
|
|
35
38
|
]
|
|
36
39
|
}
|
package/package.json
CHANGED
package/src/client/MarketTab.tsx
CHANGED
|
@@ -48,6 +48,7 @@ export function MarketTab(props: { t: Translate; market: MarketInjected; mcp: Mc
|
|
|
48
48
|
const [busyId, setBusyId] = useState<string | null>(null)
|
|
49
49
|
const [outcome, setOutcome] = useState<{ ok: boolean; text: string } | null>(null)
|
|
50
50
|
const [confirmUninstall, setConfirmUninstall] = useState<{ kind: 'root' | 'mcp'; id: string; name: string } | null>(null)
|
|
51
|
+
const [detail, setDetail] = useState<{ kind: Segment; id: string } | null>(null)
|
|
51
52
|
const [reload, setReload] = useState(0)
|
|
52
53
|
|
|
53
54
|
const refreshSkills = useCallback((): void => {
|
|
@@ -129,6 +130,13 @@ export function MarketTab(props: { t: Translate; market: MarketInjected; mcp: Mc
|
|
|
129
130
|
const title = (entry: { name: string; nameZh?: string }): string => pick(entry.name, entry.nameZh)
|
|
130
131
|
const desc = (entry: { description: string; descriptionZh?: string }): string => pick(entry.description, entry.descriptionZh)
|
|
131
132
|
|
|
133
|
+
const cardStop = (event: { stopPropagation(): void }): void => { event.stopPropagation() }
|
|
134
|
+
|
|
135
|
+
// The card list refreshes on install/uninstall, so the detail modal resolves
|
|
136
|
+
// its entry by id each render and follows the fresh installed state.
|
|
137
|
+
const detailRepo = detail !== null && detail.kind === 'skills' && repos !== null ? repos.find(repo => repo.id === detail.id) ?? null : null
|
|
138
|
+
const detailServer = detail !== null && detail.kind === 'mcp' && servers !== null ? servers.find(server => server.id === detail.id) ?? null : null
|
|
139
|
+
|
|
132
140
|
return (
|
|
133
141
|
<div className="dpc-section">
|
|
134
142
|
<style>{CSS}</style>
|
|
@@ -172,7 +180,7 @@ export function MarketTab(props: { t: Translate; market: MarketInjected; mcp: Mc
|
|
|
172
180
|
{repos !== null && repos.length > 0 && (
|
|
173
181
|
<ul className="dpc-cards">
|
|
174
182
|
{repos.map((repo) => (
|
|
175
|
-
<li className="dpc-card" key={repo.id}>
|
|
183
|
+
<li className="dpc-card" key={repo.id} onClick={() => setDetail({ kind: 'skills', id: repo.id })}>
|
|
176
184
|
<div className="dpc-cardTop">
|
|
177
185
|
<strong className="dpc-cardTitle" title={repo.url}>{title(repo)}</strong>
|
|
178
186
|
{repo.skillCount !== undefined && <span className="dpc-tag">{t('marketSkillCount').replace('{n}', String(repo.skillCount))}</span>}
|
|
@@ -180,14 +188,15 @@ export function MarketTab(props: { t: Translate; market: MarketInjected; mcp: Mc
|
|
|
180
188
|
</div>
|
|
181
189
|
<p className="dpc-cardDesc">{desc(repo)}</p>
|
|
182
190
|
<div className="dpc-cardRow">
|
|
183
|
-
<
|
|
191
|
+
<button type="button" className="dpc-link" onClick={() => setDetail({ kind: 'skills', id: repo.id })}>{t('marketDetail')}</button>
|
|
192
|
+
<a className="dpc-link" href={repo.homepage ?? repo.url} target="_blank" rel="noreferrer" onClick={cardStop}>{t('marketHome')}</a>
|
|
184
193
|
<span className="dpc-spacer" />
|
|
185
194
|
{repo.installedId !== null ? (
|
|
186
|
-
<Button variant="ghost" size="sm" disabled={busyId !== null} onClick={() => setConfirmUninstall({ kind: 'root', id: repo.installedId as string, name: title(repo) })}>
|
|
195
|
+
<Button variant="ghost" size="sm" disabled={busyId !== null} onClick={(event) => { event.stopPropagation(); setConfirmUninstall({ kind: 'root', id: repo.installedId as string, name: title(repo) }) }}>
|
|
187
196
|
{t('marketUninstall')}
|
|
188
197
|
</Button>
|
|
189
198
|
) : (
|
|
190
|
-
<Button variant="primary" size="sm" disabled={busyId !== null} onClick={() => void doInstallRepo(repo)}>
|
|
199
|
+
<Button variant="primary" size="sm" disabled={busyId !== null} onClick={(event) => { event.stopPropagation(); void doInstallRepo(repo) }}>
|
|
191
200
|
{busyId === repo.id ? t('marketInstalling') : t('marketInstall')}
|
|
192
201
|
</Button>
|
|
193
202
|
)}
|
|
@@ -207,7 +216,7 @@ export function MarketTab(props: { t: Translate; market: MarketInjected; mcp: Mc
|
|
|
207
216
|
{servers !== null && servers.length > 0 && (
|
|
208
217
|
<ul className="dpc-cards">
|
|
209
218
|
{servers.map((server) => (
|
|
210
|
-
<li className="dpc-card" key={server.id}>
|
|
219
|
+
<li className="dpc-card" key={server.id} onClick={() => setDetail({ kind: 'mcp', id: server.id })}>
|
|
211
220
|
<div className="dpc-cardTop">
|
|
212
221
|
<strong className="dpc-cardTitle" title={server.id}>{title(server)}</strong>
|
|
213
222
|
<span className="dpc-tag">{server.transport}</span>
|
|
@@ -219,20 +228,21 @@ export function MarketTab(props: { t: Translate; market: MarketInjected; mcp: Mc
|
|
|
219
228
|
<p className="dpc-envHint">{t('marketNeedsEnv')}: {server.envKeys.join(', ')}</p>
|
|
220
229
|
)}
|
|
221
230
|
<div className="dpc-cardRow">
|
|
222
|
-
<
|
|
231
|
+
<button type="button" className="dpc-link" onClick={() => setDetail({ kind: 'mcp', id: server.id })}>{t('marketDetail')}</button>
|
|
232
|
+
<a className="dpc-link" href={server.homepage} target="_blank" rel="noreferrer" onClick={cardStop}>{t('marketHome')}</a>
|
|
223
233
|
<span className="dpc-spacer" />
|
|
224
234
|
{server.installed ? (
|
|
225
|
-
<Button variant="ghost" size="sm" disabled={busyId !== null} onClick={() => void mcp.list().then(
|
|
235
|
+
<Button variant="ghost" size="sm" disabled={busyId !== null} onClick={(event) => { event.stopPropagation(); void mcp.list().then(
|
|
226
236
|
(body) => {
|
|
227
237
|
const row = body.servers.find(item => item.serverName === server.id)
|
|
228
238
|
if (row !== undefined) setConfirmUninstall({ kind: 'mcp', id: row.id, name: server.id })
|
|
229
239
|
},
|
|
230
240
|
(error: Error) => setOutcome({ ok: false, text: `${t('failed')}: ${String(error.message ?? error)}` }),
|
|
231
|
-
)}>
|
|
241
|
+
) }}>
|
|
232
242
|
{t('marketUninstall')}
|
|
233
243
|
</Button>
|
|
234
244
|
) : (
|
|
235
|
-
<Button variant="primary" size="sm" disabled={busyId !== null} onClick={() => void doInstallMcp(server)}>
|
|
245
|
+
<Button variant="primary" size="sm" disabled={busyId !== null} onClick={(event) => { event.stopPropagation(); void doInstallMcp(server) }}>
|
|
236
246
|
{busyId === server.id ? t('marketInstalling') : t('marketAdd')}
|
|
237
247
|
</Button>
|
|
238
248
|
)}
|
|
@@ -244,6 +254,94 @@ export function MarketTab(props: { t: Translate; market: MarketInjected; mcp: Mc
|
|
|
244
254
|
</>
|
|
245
255
|
)}
|
|
246
256
|
|
|
257
|
+
<Modal
|
|
258
|
+
open={detailRepo !== null || detailServer !== null}
|
|
259
|
+
onClose={() => setDetail(null)}
|
|
260
|
+
title={detailRepo !== null ? title(detailRepo) : detailServer !== null ? title(detailServer) : ''}
|
|
261
|
+
footer={
|
|
262
|
+
<>
|
|
263
|
+
{detailRepo !== null && (
|
|
264
|
+
<a className="dpc-link" href={detailRepo.homepage ?? detailRepo.url} target="_blank" rel="noreferrer">{t('marketHome')}</a>
|
|
265
|
+
)}
|
|
266
|
+
{detailServer !== null && (
|
|
267
|
+
<a className="dpc-link" href={detailServer.homepage} target="_blank" rel="noreferrer">{t('marketHome')}</a>
|
|
268
|
+
)}
|
|
269
|
+
<span className="dpc-spacer" />
|
|
270
|
+
{detailRepo !== null && (detailRepo.installedId !== null ? (
|
|
271
|
+
<Button variant="ghost" disabled={busyId !== null} onClick={() => { setConfirmUninstall({ kind: 'root', id: detailRepo.installedId as string, name: title(detailRepo) }); setDetail(null) }}>
|
|
272
|
+
{t('marketUninstall')}
|
|
273
|
+
</Button>
|
|
274
|
+
) : (
|
|
275
|
+
<Button variant="primary" disabled={busyId !== null} onClick={() => void doInstallRepo(detailRepo)}>
|
|
276
|
+
{busyId === detailRepo.id ? t('marketInstalling') : t('marketInstall')}
|
|
277
|
+
</Button>
|
|
278
|
+
))}
|
|
279
|
+
{detailServer !== null && (detailServer.installed ? (
|
|
280
|
+
<Button variant="ghost" disabled={busyId !== null} onClick={() => void mcp.list().then(
|
|
281
|
+
(body) => {
|
|
282
|
+
const row = body.servers.find(item => item.serverName === detailServer.id)
|
|
283
|
+
if (row !== undefined) { setConfirmUninstall({ kind: 'mcp', id: row.id, name: title(detailServer) }); setDetail(null) }
|
|
284
|
+
},
|
|
285
|
+
(error: Error) => setOutcome({ ok: false, text: `${t('failed')}: ${String(error.message ?? error)}` }),
|
|
286
|
+
)}>
|
|
287
|
+
{t('marketUninstall')}
|
|
288
|
+
</Button>
|
|
289
|
+
) : (
|
|
290
|
+
<Button variant="primary" disabled={busyId !== null} onClick={() => void doInstallMcp(detailServer)}>
|
|
291
|
+
{busyId === detailServer.id ? t('marketInstalling') : t('marketAdd')}
|
|
292
|
+
</Button>
|
|
293
|
+
))}
|
|
294
|
+
</>
|
|
295
|
+
}
|
|
296
|
+
>
|
|
297
|
+
{detailRepo !== null && (
|
|
298
|
+
<>
|
|
299
|
+
<p className="dpc-detailDesc">{desc(detailRepo)}</p>
|
|
300
|
+
{detailRepo.skills !== undefined && detailRepo.skills.length > 0 && (
|
|
301
|
+
<div className="dpc-detailSection">
|
|
302
|
+
<div className="dpc-detailLabel">{t('marketSkillListLabel')}</div>
|
|
303
|
+
<div className="dpc-detailTags">
|
|
304
|
+
{detailRepo.skills.map(name => <span className="dpc-tag" key={name}>{name}</span>)}
|
|
305
|
+
</div>
|
|
306
|
+
</div>
|
|
307
|
+
)}
|
|
308
|
+
</>
|
|
309
|
+
)}
|
|
310
|
+
{detailServer !== null && (
|
|
311
|
+
<>
|
|
312
|
+
<p className="dpc-detailDesc">{desc(detailServer)}</p>
|
|
313
|
+
{detailServer.command !== undefined && (
|
|
314
|
+
<div className="dpc-detailSection">
|
|
315
|
+
<div className="dpc-detailLabel">{t('marketCommandLabel')}</div>
|
|
316
|
+
<code className="dpc-code">{[detailServer.command, ...(detailServer.args ?? [])].join(' ')}</code>
|
|
317
|
+
</div>
|
|
318
|
+
)}
|
|
319
|
+
{detailServer.transport === 'streamable-http' && detailServer.url !== undefined && (
|
|
320
|
+
<div className="dpc-detailSection">
|
|
321
|
+
<div className="dpc-detailLabel">{t('marketCommandLabel')}</div>
|
|
322
|
+
<code className="dpc-code">{detailServer.url}</code>
|
|
323
|
+
</div>
|
|
324
|
+
)}
|
|
325
|
+
{detailServer.envKeys !== undefined && detailServer.envKeys.length > 0 && (
|
|
326
|
+
<div className="dpc-detailSection">
|
|
327
|
+
<div className="dpc-detailLabel">{t('marketNeedsEnv')}</div>
|
|
328
|
+
<div className="dpc-detailTags">
|
|
329
|
+
{detailServer.envKeys.map(key => <span className="dpc-tag" key={key}>{key}</span>)}
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
)}
|
|
333
|
+
{detailServer.tools !== undefined && detailServer.tools.length > 0 && (
|
|
334
|
+
<div className="dpc-detailSection">
|
|
335
|
+
<div className="dpc-detailLabel">{t('marketToolsLabel')}</div>
|
|
336
|
+
<div className="dpc-detailTags">
|
|
337
|
+
{detailServer.tools.map(name => <span className="dpc-tag" key={name}>{name}</span>)}
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
)}
|
|
341
|
+
</>
|
|
342
|
+
)}
|
|
343
|
+
</Modal>
|
|
344
|
+
|
|
247
345
|
<Modal
|
|
248
346
|
open={confirmUninstall !== null}
|
|
249
347
|
onClose={() => setConfirmUninstall(null)}
|
package/src/client/css.ts
CHANGED
|
@@ -26,7 +26,7 @@ export const CSS = `
|
|
|
26
26
|
.dpc-refresh:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:-2px}
|
|
27
27
|
.dpc-empty{margin:0;font-size:13px;line-height:20px;color:var(--dsw-alias-label-tertiary)}
|
|
28
28
|
.dpc-cards{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));align-items:stretch;gap:10px;margin:0;padding:0;list-style:none}
|
|
29
|
-
.dpc-card{display:flex;flex-direction:column;gap:8px;min-width:0;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-3);padding:12px 14px}
|
|
29
|
+
.dpc-card{display:flex;flex-direction:column;gap:8px;min-width:0;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-3);padding:12px 14px;cursor:pointer}
|
|
30
30
|
.dpc-card:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
31
31
|
.dpc-cardTop{display:flex;align-items:center;gap:8px}
|
|
32
32
|
.dpc-cardTitle{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14px;line-height:20px;font-weight:600;font-family:var(--ds-font-family-code)}
|
|
@@ -108,5 +108,10 @@ export const CSS = `
|
|
|
108
108
|
.dpc-format .dpc-form{margin-top:8px}
|
|
109
109
|
.dpc-formatHint{margin:2px 0 0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}
|
|
110
110
|
.dpc-code{margin:4px 0 8px;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:8px 10px;overflow-x:auto;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);font-family:var(--ds-font-family-code);font-size:11px;line-height:17px;white-space:pre}
|
|
111
|
+
/* Market detail modal. */
|
|
112
|
+
.dpc-detailDesc{margin:0;font-size:13px;line-height:20px}
|
|
113
|
+
.dpc-detailSection{margin-top:12px;display:flex;flex-direction:column;gap:6px}
|
|
114
|
+
.dpc-detailLabel{font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary)}
|
|
115
|
+
.dpc-detailTags{display:flex;flex-wrap:wrap;gap:4px}
|
|
111
116
|
@media(max-width:680px){.dpc-cards{grid-template-columns:minmax(0,1fr)}}
|
|
112
117
|
`
|
package/src/client/index.ts
CHANGED
|
@@ -141,6 +141,7 @@ export interface MarketRepoView {
|
|
|
141
141
|
url: string
|
|
142
142
|
homepage?: string
|
|
143
143
|
skillCount?: number
|
|
144
|
+
skills?: string[]
|
|
144
145
|
}
|
|
145
146
|
|
|
146
147
|
/** One MCP-market server as the browser sees it. */
|
|
@@ -158,4 +159,5 @@ export interface MarketServerView {
|
|
|
158
159
|
homepage: string
|
|
159
160
|
category?: string
|
|
160
161
|
runtime?: string
|
|
162
|
+
tools?: string[]
|
|
161
163
|
}
|
package/src/client/locales.ts
CHANGED
|
@@ -125,6 +125,10 @@ export const zh = {
|
|
|
125
125
|
marketRepoInstalled: '仓库已安装,技能稍候出现在「技能」页列表。',
|
|
126
126
|
marketUninstallConfirm: '确认移除?',
|
|
127
127
|
marketUninstallRootWarn: '将取消扫描该仓库并删除下载的副本(移除后可重新安装)。',
|
|
128
|
+
marketDetail: '详情',
|
|
129
|
+
marketSkillListLabel: '技能清单',
|
|
130
|
+
marketToolsLabel: '提供工具',
|
|
131
|
+
marketCommandLabel: '启动命令',
|
|
128
132
|
}
|
|
129
133
|
|
|
130
134
|
export const en = {
|
|
@@ -252,4 +256,8 @@ export const en = {
|
|
|
252
256
|
marketRepoInstalled: 'Repository installed — its skills appear on the Skills tab shortly.',
|
|
253
257
|
marketUninstallConfirm: 'Uninstall?',
|
|
254
258
|
marketUninstallRootWarn: 'Stops scanning the repository and deletes the downloaded copy (you can reinstall later).',
|
|
259
|
+
marketDetail: 'Details',
|
|
260
|
+
marketSkillListLabel: 'Skills included',
|
|
261
|
+
marketToolsLabel: 'Tools provided',
|
|
262
|
+
marketCommandLabel: 'Launch command',
|
|
255
263
|
}
|
package/src/market.test.ts
CHANGED
|
@@ -8,10 +8,11 @@ const okJson = (body: unknown): typeof fetch =>
|
|
|
8
8
|
describe('loadMarketIndex', () => {
|
|
9
9
|
it('prefers a valid remote index', async () => {
|
|
10
10
|
const index = await loadMarketIndex('skills', {
|
|
11
|
-
fetcher: okJson({ repos: [{ id: 'r1', name: 'Repo', description: 'd', url: 'https://github.com/a/b' }] }),
|
|
11
|
+
fetcher: okJson({ repos: [{ id: 'r1', name: 'Repo', description: 'd', url: 'https://github.com/a/b', skills: ['s1', 's2'] }] }),
|
|
12
12
|
})
|
|
13
13
|
expect(index?.source).toBe('remote')
|
|
14
14
|
expect(index?.skills?.[0]?.id).toBe('r1')
|
|
15
|
+
expect(index?.skills?.[0]?.skills).toEqual(['s1', 's2'])
|
|
15
16
|
})
|
|
16
17
|
|
|
17
18
|
it('falls back to the bundled snapshot when remote is unreachable or invalid', async () => {
|
|
@@ -29,7 +30,7 @@ describe('loadMarketIndex', () => {
|
|
|
29
30
|
const index = await loadMarketIndex('mcp', {
|
|
30
31
|
fetcher: okJson({
|
|
31
32
|
servers: [
|
|
32
|
-
{ id: 'ok', name: 'OK', description: 'd', transport: 'stdio', command: 'npx', args: ['-y', 'x'], envKeys: ['K'], homepage: 'https://x' },
|
|
33
|
+
{ id: 'ok', name: 'OK', description: 'd', transport: 'stdio', command: 'npx', args: ['-y', 'x'], envKeys: ['K'], homepage: 'https://x', tools: ['t1'] },
|
|
33
34
|
{ id: 'no-command', name: 'Bad', description: 'd', transport: 'stdio', homepage: 'https://x' },
|
|
34
35
|
{ id: 'no-home', name: 'Bad2', description: 'd', transport: 'stdio', command: 'npx' },
|
|
35
36
|
'not an object',
|
|
@@ -38,7 +39,7 @@ describe('loadMarketIndex', () => {
|
|
|
38
39
|
})
|
|
39
40
|
expect(index?.source).toBe('remote')
|
|
40
41
|
expect(index?.servers).toHaveLength(1)
|
|
41
|
-
expect(index?.servers?.[0]).toMatchObject({ id: 'ok', envKeys: ['K'] })
|
|
42
|
+
expect(index?.servers?.[0]).toMatchObject({ id: 'ok', envKeys: ['K'], tools: ['t1'] })
|
|
42
43
|
})
|
|
43
44
|
|
|
44
45
|
it('ships parseable bundled snapshots with expected top-level shape', () => {
|
|
@@ -54,4 +55,12 @@ describe('loadMarketIndex', () => {
|
|
|
54
55
|
const index = await loadMarketIndex('mcp', { fetcher: okJson({ servers: [{ id: 'http1', name: 'H', description: 'd', transport: 'streamable-http', url: 'https://e/mcp', homepage: 'https://x' }] }) })
|
|
55
56
|
expect(index?.servers?.[0]).toMatchObject({ transport: 'streamable-http', url: 'https://e/mcp' })
|
|
56
57
|
})
|
|
58
|
+
|
|
59
|
+
it('bundles skill and tool inventories for the market detail view', async () => {
|
|
60
|
+
const failing = (async () => { throw new Error('offline') }) as unknown as typeof fetch
|
|
61
|
+
const skills = await loadMarketIndex('skills', { fetcher: failing })
|
|
62
|
+
expect(skills?.skills?.some(repo => (repo.skills?.length ?? 0) > 0)).toBe(true)
|
|
63
|
+
const mcp = await loadMarketIndex('mcp', { fetcher: failing })
|
|
64
|
+
expect(mcp?.servers?.some(server => (server.tools?.length ?? 0) > 0)).toBe(true)
|
|
65
|
+
})
|
|
57
66
|
})
|
package/src/market.ts
CHANGED
|
@@ -23,6 +23,8 @@ export interface MarketSkillRepo {
|
|
|
23
23
|
url: string
|
|
24
24
|
homepage?: string
|
|
25
25
|
skillCount?: number
|
|
26
|
+
/** Skill names inside the repository, for the market detail view. */
|
|
27
|
+
skills?: string[]
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
/** One installable server in the MCP index. */
|
|
@@ -41,6 +43,8 @@ export interface MarketMcpServer {
|
|
|
41
43
|
category?: string
|
|
42
44
|
/** Runtime hint shown next to stdio commands (npx / uvx). */
|
|
43
45
|
runtime?: string
|
|
46
|
+
/** Tool names the server exposes, for the market detail view. */
|
|
47
|
+
tools?: string[]
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
export interface MarketIndex {
|
|
@@ -80,6 +84,7 @@ function parseSkillsIndex(parsed: unknown): MarketSkillRepo[] | null {
|
|
|
80
84
|
url: record.url,
|
|
81
85
|
...isString(record.homepage) ? { homepage: record.homepage } : {},
|
|
82
86
|
...(typeof record.skillCount === 'number' ? { skillCount: record.skillCount } : {}),
|
|
87
|
+
...isStringArray(record.skills) ? { skills: record.skills } : {},
|
|
83
88
|
})
|
|
84
89
|
}
|
|
85
90
|
return out.length > 0 ? out : null
|
|
@@ -111,6 +116,7 @@ function parseMcpIndex(parsed: unknown): MarketMcpServer[] | null {
|
|
|
111
116
|
homepage: record.homepage,
|
|
112
117
|
...isString(record.category) ? { category: record.category } : {},
|
|
113
118
|
...isString(record.runtime) ? { runtime: record.runtime } : {},
|
|
119
|
+
...isStringArray(record.tools) ? { tools: record.tools } : {},
|
|
114
120
|
})
|
|
115
121
|
}
|
|
116
122
|
return out.length > 0 ? out : null
|
package/src/opener.ts
CHANGED
|
@@ -15,9 +15,17 @@ export function openDirectory(dir: string): boolean {
|
|
|
15
15
|
} catch {
|
|
16
16
|
return false
|
|
17
17
|
}
|
|
18
|
+
if (process.platform === 'win32') {
|
|
19
|
+
// explorer 对正斜杠路径会静默回落到默认文件夹(实测打开成了「文档」),必须喂反斜杠。
|
|
20
|
+
dir = dir.split('/').join('\\')
|
|
21
|
+
}
|
|
18
22
|
const launcher = process.platform === 'win32' ? 'explorer' : process.platform === 'darwin' ? 'open' : 'xdg-open'
|
|
19
23
|
try {
|
|
20
|
-
|
|
24
|
+
// GUI 启动器不能带 windowsHide:该 flag 把 SW_HIDE 写进 STARTUPINFO,explorer 的
|
|
25
|
+
// 首个窗口据此创建为隐藏——实机表现是「打开目录」点了没反应,窗口其实开了。
|
|
26
|
+
const child = spawn(launcher, [dir], { detached: true, stdio: 'ignore' })
|
|
27
|
+
// 启动器解析失败时 'error' 异步到达;没有监听会升级成 uncaughtException 炸掉 sidecar。
|
|
28
|
+
child.once('error', () => {})
|
|
21
29
|
child.unref()
|
|
22
30
|
return true
|
|
23
31
|
} catch {
|
package/src/routes.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** HTTP routes bridging the Settings UI to the capabilities manager. */
|
|
2
2
|
|
|
3
3
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
4
|
+
import { mkdirSync } from 'node:fs'
|
|
4
5
|
import { scanAllMcp } from './agents.ts'
|
|
5
6
|
import { readJsonBody, sameOrigin, sendJson } from './http.ts'
|
|
6
7
|
import { loadMarketIndex, type MarketMcpServer } from './market.ts'
|
|
@@ -199,8 +200,11 @@ export function mountCapabilitiesRoutes(host: CapabilitiesHost, config: Capabili
|
|
|
199
200
|
let dir: string | undefined
|
|
200
201
|
if (body.target === 'user-skills') {
|
|
201
202
|
dir = userSkillsDir()
|
|
203
|
+
// 用户还没建过任何技能时该目录不存在;「打开技能目录」应按需创建而非报错。
|
|
204
|
+
mkdirSync(dir, { recursive: true })
|
|
202
205
|
} else if (body.target === 'plugin-state') {
|
|
203
206
|
dir = pluginStateDir()
|
|
207
|
+
mkdirSync(dir, { recursive: true })
|
|
204
208
|
} else if (body.target === 'skill') {
|
|
205
209
|
if (typeof body.name !== 'string') {
|
|
206
210
|
sendJson(response, 400, { error: 'name is required' })
|
package/src/smoke.test.ts
CHANGED
|
@@ -199,10 +199,11 @@ describe.skipIf(process.env.DSH_DESKTOP_PLUGIN_SMOKE !== '1' || !guard || !nodeO
|
|
|
199
199
|
expect(after.servers.some(row => row.serverName === 'memory')).toBe(true)
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
-
// 9. Open-folder route resolves server-side and answers ok
|
|
203
|
-
//
|
|
202
|
+
// 9. Open-folder route resolves server-side and answers ok; the skills
|
|
203
|
+
// home is created on demand, so a fresh DSH_HOME opens (not 422s).
|
|
204
204
|
const open = await post('/dsh-plugin-capabilities/open', { target: 'user-skills' })
|
|
205
|
-
expect(
|
|
205
|
+
expect(open.status).toBe(200)
|
|
206
|
+
expect(existsSync(join(smokeRoot, 'skills'))).toBe(true)
|
|
206
207
|
const openBad = await post('/dsh-plugin-capabilities/open', { target: 'root', id: 'does-not-exist' })
|
|
207
208
|
expect(openBad.status).toBe(404)
|
|
208
209
|
})
|