dsh-plugin-capabilities 0.2.1 → 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/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
  }
@@ -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
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": 18
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": 4
35
+ "skillCount": 1,
36
+ "skills": ["find-skills"]
34
37
  }
35
38
  ]
36
39
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-capabilities",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Manage skills and MCP servers from the Web UI Settings. 在设置页管理 dsh 的技能与 MCP 服务器。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -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
- <a className="dpc-link" href={repo.homepage ?? repo.url} target="_blank" rel="noreferrer">{t('marketHome')}</a>
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
- <a className="dpc-link" href={server.homepage} target="_blank" rel="noreferrer">{t('marketHome')}</a>
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
  `
@@ -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
  }
@@ -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
  }
@@ -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