@dsh-so/mcp 0.1.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 +44 -0
- package/package.json +31 -0
- package/server.mjs +326 -0
- package/skills/dsh-plugin-data/SKILL.md +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# @dsh-so/mcp
|
|
2
|
+
|
|
3
|
+
[dsh.so](https://www.dsh.so) 的 MCP(Model Context Protocol)服务器:把站点预渲染的机器可读插件数据(注册表索引、安全扫描、L1-L5 安装实测、npm 周下载榜、Star 涨幅榜)以结构化工具的形式暴露给 Agent。
|
|
4
|
+
|
|
5
|
+
零依赖、纯读取 —— 每个工具只是抓取对应的静态 JSON 端点并裁剪返回,与站点「无后端、免密钥、CORS 全开」的架构完全一致。
|
|
6
|
+
|
|
7
|
+
## 接入
|
|
8
|
+
|
|
9
|
+
在任意支持 MCP 的客户端(Claude Code / ZCode / Cursor 等)配置:
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"mcpServers": {
|
|
14
|
+
"dsh-so": {
|
|
15
|
+
"command": "npx",
|
|
16
|
+
"args": ["-y", "@dsh-so/mcp"]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
环境变量 `DSH_SO_BASE` 可覆盖数据源(默认 `https://www.dsh.so`)。
|
|
23
|
+
|
|
24
|
+
## 工具
|
|
25
|
+
|
|
26
|
+
| 工具 | 数据源 | 说明 |
|
|
27
|
+
| --- | --- | --- |
|
|
28
|
+
| `search_plugins` | `/plugins-index.json` | 关键词 + 验证等级 / 安全状态 / 风险等级过滤,默认排除生态应用 |
|
|
29
|
+
| `get_plugin_record` | `/plugin/<id>.json` | 单插件权威事实表(推荐推荐/安装前先查这个) |
|
|
30
|
+
| `get_security_scan` | `/artifact/<id>.json` | 单插件安全扫描记录 |
|
|
31
|
+
| `get_install_status` | `/data/install/<id>.json` | 单插件安装实测状态(L5 优先 / L4 兜底) |
|
|
32
|
+
| `get_npm_downloads` | `/data/npm-downloads.json` | npm 周下载 Top-100 榜 |
|
|
33
|
+
| `get_star_trend` | `/data/star-trend.json` | 一周 Star 涨幅 Top-100 榜 |
|
|
34
|
+
| `data_freshness` | 多端点 | 各核心数据源的新鲜度标记(generatedAt / snapshotDate) |
|
|
35
|
+
|
|
36
|
+
端点完整语义见 <https://www.dsh.so/data/>。
|
|
37
|
+
|
|
38
|
+
## Agent Skill
|
|
39
|
+
|
|
40
|
+
本包自带 [SKILL.md](skills/dsh-plugin-data/SKILL.md)(`dsh-plugin-data`),教 Agent 在推荐 / 安装 DSH 插件前先核验安全与安装状态。不配 MCP、只用 skill 也行 —— skill 会指引 Agent 直接 fetch 静态端点。站内直链:<https://www.dsh.so/skills/dsh-plugin-data/SKILL.md>。
|
|
41
|
+
|
|
42
|
+
## 许可
|
|
43
|
+
|
|
44
|
+
代码 MIT;数据 CC BY 4.0 —— 转发必须保留 `generatedAt` / `asOf` / `checkedAt` 新鲜度标记并署名 dsh.so。
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dsh-so/mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server exposing dsh.so machine-readable DSH plugin data: registry index, security scans, install verification (L1-L5), npm download and star trend boards.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"dsh-so-mcp": "./server.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"server.mjs",
|
|
11
|
+
"skills",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"mcp",
|
|
19
|
+
"model-context-protocol",
|
|
20
|
+
"dsh",
|
|
21
|
+
"deepseek",
|
|
22
|
+
"plugin-registry",
|
|
23
|
+
"agent"
|
|
24
|
+
],
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/ihuajiu/dsh.so.git",
|
|
29
|
+
"directory": "mcp"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/server.mjs
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @dsh-so/mcp — stdio MCP server exposing dsh.so machine-readable plugin data.
|
|
3
|
+
// Zero-dependency by design: the site has no backend, every tool just reads a
|
|
4
|
+
// prerendered static JSON endpoint and returns a (trimmed) slice of it.
|
|
5
|
+
// Transport: newline-delimited JSON-RPC 2.0 on stdio (MCP stdio transport).
|
|
6
|
+
|
|
7
|
+
import readline from 'node:readline';
|
|
8
|
+
|
|
9
|
+
const VERSION = '0.1.0';
|
|
10
|
+
const BASE = (process.env.DSH_SO_BASE || 'https://www.dsh.so').replace(/\/+$/, '');
|
|
11
|
+
const PROTOCOL_VERSION = '2025-06-18';
|
|
12
|
+
|
|
13
|
+
/* ------------------------------------------------------------------ */
|
|
14
|
+
/* HTTP */
|
|
15
|
+
/* ------------------------------------------------------------------ */
|
|
16
|
+
|
|
17
|
+
async function fetchJson(path, retried = false) {
|
|
18
|
+
const controller = new AbortController();
|
|
19
|
+
const timer = setTimeout(() => controller.abort(), 15_000);
|
|
20
|
+
try {
|
|
21
|
+
const res = await fetch(BASE + path, {
|
|
22
|
+
headers: { accept: 'application/json' },
|
|
23
|
+
signal: controller.signal,
|
|
24
|
+
})
|
|
25
|
+
if (res.status === 404) {
|
|
26
|
+
throw Object.assign(new Error(`Not found: ${BASE}${path} — check the plugin id (form: author/plugin or topic-slug).`), { statusCode: 404 })
|
|
27
|
+
}
|
|
28
|
+
if (!res.ok) {
|
|
29
|
+
throw Object.assign(new Error(`Upstream ${res.status} for ${path}`), { statusCode: res.status })
|
|
30
|
+
}
|
|
31
|
+
return await res.json()
|
|
32
|
+
} catch (err) {
|
|
33
|
+
if (err.name === 'AbortError') {
|
|
34
|
+
throw Object.assign(new Error(`Timeout fetching ${path} after 15s`), { statusCode: 504 })
|
|
35
|
+
}
|
|
36
|
+
// Transient network-level failure ("fetch failed"): the site is static
|
|
37
|
+
// behind a CDN, so a single retry settles nearly all of them.
|
|
38
|
+
if (!err.statusCode && !retried) {
|
|
39
|
+
await new Promise((resolve) => setTimeout(resolve, 300))
|
|
40
|
+
return fetchJson(path, true)
|
|
41
|
+
}
|
|
42
|
+
throw err
|
|
43
|
+
} finally {
|
|
44
|
+
clearTimeout(timer)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* ------------------------------------------------------------------ */
|
|
49
|
+
/* Shapers — keep tool output small, agents pay per token */
|
|
50
|
+
/* ------------------------------------------------------------------ */
|
|
51
|
+
|
|
52
|
+
function pluginSummary(p) {
|
|
53
|
+
return {
|
|
54
|
+
id: p.id,
|
|
55
|
+
name: p.name,
|
|
56
|
+
description: p.description,
|
|
57
|
+
stars: p.stars ?? null,
|
|
58
|
+
install: p.install ?? null,
|
|
59
|
+
kind: p.kind || null,
|
|
60
|
+
verificationLevel: p.verification?.level ?? null,
|
|
61
|
+
lastVerifiedAt: p.verification?.lastVerifiedAt ?? null,
|
|
62
|
+
securityStatus: p.security?.status ?? null,
|
|
63
|
+
riskLevel: p.security?.riskLevel ?? null,
|
|
64
|
+
record: p.evidence?.record ?? `${BASE}/plugin/${p.id}.json`,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function feedSlice(feed, limit) {
|
|
69
|
+
const items = Array.isArray(feed.items) ? feed.items : []
|
|
70
|
+
return {
|
|
71
|
+
generatedAt: feed.generatedAt ?? null,
|
|
72
|
+
snapshotDate: feed.snapshotDate ?? null,
|
|
73
|
+
window: feed.window ?? null,
|
|
74
|
+
limit: feed.limit ?? null,
|
|
75
|
+
count: items.length,
|
|
76
|
+
items: items.slice(0, limit),
|
|
77
|
+
license: feed.license ?? null,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* ------------------------------------------------------------------ */
|
|
82
|
+
/* Tools */
|
|
83
|
+
/* ------------------------------------------------------------------ */
|
|
84
|
+
|
|
85
|
+
const TOOLS = [
|
|
86
|
+
{
|
|
87
|
+
name: 'search_plugins',
|
|
88
|
+
title: 'Search the dsh.so plugin registry',
|
|
89
|
+
description:
|
|
90
|
+
'Search the dsh.so plugin registry index. Filter by keyword, minimum install-verification level (L1-L5), security status or risk level. Ecosystem apps (self-contained shells, not `dsh plugin add`-installable) are excluded by default.',
|
|
91
|
+
inputSchema: {
|
|
92
|
+
type: 'object',
|
|
93
|
+
properties: {
|
|
94
|
+
query: { type: 'string', description: 'Keyword matched against id, name and description (case-insensitive).' },
|
|
95
|
+
minVerificationLevel: { type: 'integer', minimum: 1, maximum: 5, description: 'Only return plugins whose L-level >= this value (5 = smoke-tested install).' },
|
|
96
|
+
securityStatus: { type: 'string', enum: ['passed', 'warning', 'high-risk', 'critical'], description: 'Security scan status filter.' },
|
|
97
|
+
riskLevel: { type: 'string', enum: ['low', 'medium', 'high', 'unknown'], description: 'Risk level filter.' },
|
|
98
|
+
includeApps: { type: 'boolean', default: false, description: 'Include ecosystem apps (kind: ecosystem-app, not plugin-installable).' },
|
|
99
|
+
limit: { type: 'integer', minimum: 1, maximum: 100, default: 20, description: 'Max results (sorted by GitHub stars, descending).' },
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
async run(args = {}) {
|
|
103
|
+
const index = await fetchJson('/plugins-index.json')
|
|
104
|
+
const q = (args.query || '').toLowerCase()
|
|
105
|
+
const minLevel = Number.isInteger(args.minVerificationLevel) ? args.minVerificationLevel : 0
|
|
106
|
+
const includeApps = args.includeApps === true
|
|
107
|
+
const limit = Number.isInteger(args.limit) ? Math.min(args.limit, 100) : 20
|
|
108
|
+
const all = Array.isArray(index.plugins) ? index.plugins : []
|
|
109
|
+
const hits = all
|
|
110
|
+
.filter((p) => !includeApps && p.install === null ? false : true)
|
|
111
|
+
.filter((p) => (p.verification?.level ?? 0) >= minLevel)
|
|
112
|
+
.filter((p) => !args.securityStatus || p.security?.status === args.securityStatus)
|
|
113
|
+
.filter((p) => !args.riskLevel || p.security?.riskLevel === args.riskLevel)
|
|
114
|
+
.filter((p) => !q || `${p.id} ${p.name} ${p.description}`.toLowerCase().includes(q))
|
|
115
|
+
.sort((a, b) => (b.stars || 0) - (a.stars || 0))
|
|
116
|
+
.slice(0, limit)
|
|
117
|
+
.map(pluginSummary)
|
|
118
|
+
return {
|
|
119
|
+
generatedAt: index.generatedAt ?? null,
|
|
120
|
+
matched: hits.length,
|
|
121
|
+
totalInIndex: index.count ?? null,
|
|
122
|
+
plugins: hits,
|
|
123
|
+
license: index.license ?? null,
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: 'get_plugin_record',
|
|
129
|
+
title: 'Get the canonical per-plugin fact sheet',
|
|
130
|
+
description:
|
|
131
|
+
'Fetch the canonical dsh.so Plugin Record for one plugin: verification per dsh version (L5-first), security summary, install status, repo health and evidence pointers. The single best tool for "can I trust and install this plugin?".',
|
|
132
|
+
inputSchema: {
|
|
133
|
+
type: 'object',
|
|
134
|
+
required: ['id'],
|
|
135
|
+
properties: {
|
|
136
|
+
id: { type: 'string', description: 'Plugin id, e.g. "@dsh-so/dsh-plugin-finder" or "modlens".' },
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
async run(args) {
|
|
140
|
+
return fetchJson(`/plugin/${encodeURIComponent(args.id)}.json`)
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: 'get_security_scan',
|
|
145
|
+
title: 'Get one plugin\'s security scan result',
|
|
146
|
+
description:
|
|
147
|
+
'Fetch the static security review record for one plugin: verdict (pass/warn/fail), finding counts by severity, findings and the scanned commit/version binding.',
|
|
148
|
+
inputSchema: {
|
|
149
|
+
type: 'object',
|
|
150
|
+
required: ['id'],
|
|
151
|
+
properties: {
|
|
152
|
+
id: { type: 'string', description: 'Plugin id.' },
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
async run(args) {
|
|
156
|
+
return fetchJson(`/artifact/${encodeURIComponent(args.id)}.json`)
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: 'get_install_status',
|
|
161
|
+
title: 'Get one plugin\'s install-verification status',
|
|
162
|
+
description:
|
|
163
|
+
'Fetch the install-test status for one plugin: L5 smoke preferred / L4 sandbox fallback, states passed / failed / unknown / stale / untested, with a verified flag and environment metadata.',
|
|
164
|
+
inputSchema: {
|
|
165
|
+
type: 'object',
|
|
166
|
+
required: ['id'],
|
|
167
|
+
properties: {
|
|
168
|
+
id: { type: 'string', description: 'Plugin id.' },
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
async run(args) {
|
|
172
|
+
return fetchJson(`/data/install/${encodeURIComponent(args.id)}.json`)
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
name: 'get_npm_downloads',
|
|
177
|
+
title: 'Get the weekly npm download ranking',
|
|
178
|
+
description:
|
|
179
|
+
'Fetch the top-100 DSH plugins ranked by npm downloads over the last-week rolling window — the data behind the homepage npm downloads board.',
|
|
180
|
+
inputSchema: {
|
|
181
|
+
type: 'object',
|
|
182
|
+
properties: {
|
|
183
|
+
limit: { type: 'integer', minimum: 1, maximum: 100, default: 20, description: 'Max entries to return.' },
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
async run(args = {}) {
|
|
187
|
+
const limit = Number.isInteger(args.limit) ? Math.min(args.limit, 100) : 20
|
|
188
|
+
return feedSlice(await fetchJson('/data/npm-downloads.json'), limit)
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
name: 'get_star_trend',
|
|
193
|
+
title: 'Get the GitHub star riser ranking',
|
|
194
|
+
description:
|
|
195
|
+
'Fetch the top-100 DSH plugins by GitHub star gain over a 1-week window, with baseline and current star counts — the data behind the homepage star board.',
|
|
196
|
+
inputSchema: {
|
|
197
|
+
type: 'object',
|
|
198
|
+
properties: {
|
|
199
|
+
limit: { type: 'integer', minimum: 1, maximum: 100, default: 20, description: 'Max entries to return.' },
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
async run(args = {}) {
|
|
203
|
+
const limit = Number.isInteger(args.limit) ? Math.min(args.limit, 100) : 20
|
|
204
|
+
return feedSlice(await fetchJson('/data/star-trend.json'), limit)
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
name: 'data_freshness',
|
|
209
|
+
title: 'Check data freshness markers',
|
|
210
|
+
description:
|
|
211
|
+
'Fetch the freshness markers (generatedAt / snapshotDate / counts) of the core dsh.so feeds. All dsh.so data is CC BY 4.0 — these markers must be preserved when redistributing.',
|
|
212
|
+
inputSchema: { type: 'object', properties: {} },
|
|
213
|
+
async run() {
|
|
214
|
+
const [index, downloads, stars] = await Promise.all([
|
|
215
|
+
fetchJson('/plugins-index.json'),
|
|
216
|
+
fetchJson('/data/npm-downloads.json'),
|
|
217
|
+
fetchJson('/data/star-trend.json'),
|
|
218
|
+
])
|
|
219
|
+
return {
|
|
220
|
+
baseUrl: BASE,
|
|
221
|
+
registryIndex: { generatedAt: index.generatedAt ?? null, count: index.count ?? null },
|
|
222
|
+
npmDownloads: { generatedAt: downloads.generatedAt ?? null, snapshotDate: downloads.snapshotDate ?? null },
|
|
223
|
+
starTrend: { generatedAt: stars.generatedAt ?? null, snapshotDate: stars.snapshotDate ?? null },
|
|
224
|
+
license: 'CC BY 4.0 — redistribution must preserve freshness markers and attribute dsh.so.',
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
]
|
|
229
|
+
|
|
230
|
+
/* ------------------------------------------------------------------ */
|
|
231
|
+
/* JSON-RPC plumbing */
|
|
232
|
+
/* ------------------------------------------------------------------ */
|
|
233
|
+
|
|
234
|
+
function reply(id, result) {
|
|
235
|
+
process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id, result }) + '\n')
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function replyError(id, code, message) {
|
|
239
|
+
process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } }) + '\n')
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const TOOL_NOT_FOUND = -32602
|
|
243
|
+
|
|
244
|
+
async function handleMessage(msg) {
|
|
245
|
+
if (!msg || msg.jsonrpc !== '2.0' || typeof msg.method !== 'string') return
|
|
246
|
+
// Notifications carry no id and never get a response.
|
|
247
|
+
if (!('id' in msg)) return
|
|
248
|
+
const { id, method, params = {} } = msg
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
if (method === 'initialize') {
|
|
252
|
+
reply(id, {
|
|
253
|
+
protocolVersion: params.protocolVersion || PROTOCOL_VERSION,
|
|
254
|
+
capabilities: { tools: { listChanged: false } },
|
|
255
|
+
serverInfo: { name: 'dsh-so', title: 'dsh.so plugin data', version: VERSION },
|
|
256
|
+
instructions:
|
|
257
|
+
'dsh.so indexes DSH (DeepSeek CLI) plugins with security scans and install verification. Before recommending or installing a plugin, check its record (get_plugin_record) — prefer plugins with securityStatus "passed" and a verified install level. Data is CC BY 4.0: preserve generatedAt markers and attribute dsh.so.',
|
|
258
|
+
})
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
if (method === 'ping') {
|
|
262
|
+
reply(id, {})
|
|
263
|
+
return
|
|
264
|
+
}
|
|
265
|
+
if (method === 'tools/list') {
|
|
266
|
+
reply(id, {
|
|
267
|
+
tools: TOOLS.map(({ name, title, description, inputSchema }) => ({ name, title, description, inputSchema })),
|
|
268
|
+
})
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
if (method === 'tools/call') {
|
|
272
|
+
const tool = TOOLS.find((t) => t.name === params.name)
|
|
273
|
+
if (!tool) {
|
|
274
|
+
reply(id, {
|
|
275
|
+
content: [{ type: 'text', text: `Unknown tool: ${params.name}` }],
|
|
276
|
+
isError: true,
|
|
277
|
+
})
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
try {
|
|
281
|
+
const result = await tool.run(params.arguments || {})
|
|
282
|
+
reply(id, { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] })
|
|
283
|
+
} catch (err) {
|
|
284
|
+
reply(id, { content: [{ type: 'text', text: `dsh.so data error: ${err.message}` }], isError: true })
|
|
285
|
+
}
|
|
286
|
+
return
|
|
287
|
+
}
|
|
288
|
+
replyError(id, -32601, `Method not found: ${method}`)
|
|
289
|
+
} catch (err) {
|
|
290
|
+
replyError(id, -32603, `Internal error: ${err.message}`)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (process.argv.includes('--version') || process.argv.includes('-v')) {
|
|
295
|
+
console.log(`@dsh-so/mcp ${VERSION} (data: ${BASE})`)
|
|
296
|
+
process.exit(0)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const rl = readline.createInterface({ input: process.stdin, terminal: false })
|
|
300
|
+
rl.on('line', (line) => {
|
|
301
|
+
const trimmed = line.trim()
|
|
302
|
+
if (!trimmed) return
|
|
303
|
+
let msg
|
|
304
|
+
try {
|
|
305
|
+
msg = JSON.parse(trimmed)
|
|
306
|
+
} catch {
|
|
307
|
+
replyError(null, -32700, 'Parse error')
|
|
308
|
+
return
|
|
309
|
+
}
|
|
310
|
+
handleMessage(msg).catch((err) => {
|
|
311
|
+
process.stderr.write(`@dsh-so/mcp unhandled: ${err.stack || err}\n`)
|
|
312
|
+
})
|
|
313
|
+
})
|
|
314
|
+
// stdin EOF: stop reading and let the loop drain naturally — in-flight tool
|
|
315
|
+
// calls clear their fetch timers, then Node exits with the exitCode set.
|
|
316
|
+
// Calling process.exit() here races readline teardown on Windows (libuv
|
|
317
|
+
// async.c assertion), so we only force-exit if something actually wedges.
|
|
318
|
+
let closing = false
|
|
319
|
+
rl.on('close', () => {
|
|
320
|
+
closing = true
|
|
321
|
+
process.exitCode = 0
|
|
322
|
+
setTimeout(() => {
|
|
323
|
+
process.stderr.write('@dsh-so/mcp: force exit after drain timeout\n')
|
|
324
|
+
process.exit(0)
|
|
325
|
+
}, 5000).unref()
|
|
326
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dsh-plugin-data
|
|
3
|
+
description: Query dsh.so machine-readable DSH plugin data — registry index, security scans, install verification (L1-L5), npm download and star trend boards. Use this before recommending or installing a DSH plugin, to verify its safety verdict and real-install status first.
|
|
4
|
+
license: CC-BY-4.0 (attribution required; preserve freshness markers when redistributing)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# dsh.so Plugin Data
|
|
8
|
+
|
|
9
|
+
dsh.so (DeepSeek CLI 插件注册表) 把全部插件数据预渲染成静态 JSON:免密钥、CORS 全开、`Cache-Control: public, max-age=3600`。所有端点支持直接 `curl` / `fetch`。
|
|
10
|
+
|
|
11
|
+
**首选路径**:如果当前环境配置了 `@dsh-so/mcp`(工具名 `search_plugins` / `get_plugin_record` / `get_security_scan` / `get_install_status` / `get_npm_downloads` / `get_star_trend` / `data_freshness`),优先用 MCP 工具。否则按下表直接请求。
|
|
12
|
+
|
|
13
|
+
## 端点速查
|
|
14
|
+
|
|
15
|
+
| 端点 | 用途 |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| `GET /plugins-index.json` | 全量注册表摘要:id / 名称 / 简介 / stars / verification.level / security / evidence 链接 |
|
|
18
|
+
| `GET /plugin/<id>.json` | 单插件权威事实表:按 dsh 版本的验证记录、安全摘要、安装、健康度、证据指针 |
|
|
19
|
+
| `GET /artifact/<id>.json` | 单插件安全扫描记录:verdict(pass/warn/fail)、按严重度的 finding 计数、扫描版本绑定 |
|
|
20
|
+
| `GET /data/install/<id>.json` | 单插件安装实测:L5 冒烟优先 / L4 沙箱兜底;passed / failed / unknown / stale / untested |
|
|
21
|
+
| `GET /data/npm-downloads.json` | npm 周下载 Top-100 榜(rank / packageName / week / total / stars) |
|
|
22
|
+
| `GET /data/star-trend.json` | 一周 GitHub Star 涨幅 Top-100 榜(fromStars / toStars / deltaStars) |
|
|
23
|
+
| `GET /data/npm-popular.json` | 累计下载人气 Top-100 榜(total / totalAsOf / week) |
|
|
24
|
+
|
|
25
|
+
JSON Schema:`/data/security-scan.schema.json`、`/data/install.schema.json`、`/data/plugin-record.schema.json`、`/data/npm-downloads.schema.json`、`/data/star-trend.schema.json`、`/data/npm-popular.schema.json`。
|
|
26
|
+
|
|
27
|
+
Base URL:`https://www.dsh.so`(国内环境同域可用)。完整字段语义见 <https://www.dsh.so/data/>。
|
|
28
|
+
|
|
29
|
+
## 决策规则(推荐 / 安装前必查)
|
|
30
|
+
|
|
31
|
+
1. **先查事实表**:`GET /plugin/<id>.json`。`security.status` 必须是 `passed`;`install.verified` 必须是 `true` 且 `install.level` 为 `L4`/`L5`。任一不满足 → 明确告知用户风险,不推荐安装。
|
|
32
|
+
2. **verdict 口径**:`pass` 可装;`warn` 说明用途后由用户决定;`fail` / `critical` / `high-risk` 一律不推荐。
|
|
33
|
+
3. **stale / untested**:安装状态为 `stale`(验证过期)或 `untested` 时如实说明「未在当前 dsh 版本实测」,不要表述为「已验证」。
|
|
34
|
+
4. **生态应用**:`install: null`(kind: ecosystem-app)表示不可 `dsh plugin add`,是自带 dsh 的独立壳 —— 不要当作插件安装。
|
|
35
|
+
5. **搜索**:用 `/plugins-index.json` 过滤(关键词匹配 id/name/description,`verification.level >= N`、`security.status`、`riskLevel`),按 stars 排序取前若干条。
|
|
36
|
+
|
|
37
|
+
## 数据诚信
|
|
38
|
+
|
|
39
|
+
- 所有数据 **CC BY 4.0**:转发必须署名 dsh.so,并保留 `generatedAt` / `snapshotDate` / `asOf` / `checkedAt` 新鲜度标记 —— 不得把这些数据伪装成实时数据。
|
|
40
|
+
- 记录均为来源可溯的 append-only 档案,引用时保留原始字段,不要臆造默认值。
|
|
41
|
+
- 插件作者可对扫描结果申诉(见 /data/security/ 的 Disputes 区块)—— 结论可能随复核更新,引用时带上时间戳。
|