dsh-armory 2.1.0 → 2.2.1
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.en.md +20 -5
- package/README.md +26 -8
- package/lib/client.js +1353 -168
- package/lib/index.js +51 -9
- package/lib/mcp-catalog-en.js +244 -0
- package/lib/mcp-config.js +15 -0
- package/lib/mcp-oauth.js +1 -1
- package/lib/skill-remove.js +125 -0
- package/lib/suites-registry.js +179 -0
- package/lib/tool-ledger.js +180 -11
- package/lib/typert.js +9 -0
- package/lib/wb-awesome-market.js +1 -1
- package/lib/wb-install-state.js +1 -1
- package/lib/wb-market.js +44 -21
- package/lib/wb-registry.js +1 -1
- package/lib/whats-new-en.js +406 -0
- package/lib/whats-new.js +45 -0
- package/lib/workspace-runtime.js +1 -1
- package/package.json +2 -2
- package/docs/chunk-split-feasibility.md +0 -153
- package/docs/host-proposal-mcp-teardown.md +0 -184
- package/docs/host-proposal-skill-toggle.md +0 -166
- package/docs/prd-awesome-dsh-plugins.md +0 -105
- package/docs/prd-workbuddy-parity.md +0 -128
- package/docs/system-design-awesome.md +0 -269
- package/docs/system_design.md +0 -530
- package/docs/workbuddy-market-gap-analysis.md +0 -197
package/lib/index.js
CHANGED
|
@@ -12,7 +12,10 @@ import { admitImportedServers } from "./import-admission.js";
|
|
|
12
12
|
import { collectImportLocations, missingEnvNames, resolveImportLocation } from "./mcp-import-sources.js";
|
|
13
13
|
import { deriveMcpPhase, formatMcpLog, sanitizeMcpLog } from "./mcp-observability.js";
|
|
14
14
|
import { allMcpEntries, entriesForName, projectToolSchema, revisionOf, toolInventory } from "./mcp-registry.js";
|
|
15
|
+
// Skill 看板的卸载落盘点:来源白名单 + 根目录包含性 + 符号链接/根目录校验(lib/skill-remove.js)。
|
|
16
|
+
import { removeSkillFromDisk } from "./skill-remove.js";
|
|
15
17
|
import { WHATS_NEW } from "./whats-new.js";
|
|
18
|
+
import { attachWhatsNewEnglish } from "./whats-new-en.js";
|
|
16
19
|
import { probeMcpServersAuthorization, wbConnectorCatalog, wbConnectorCachedCount, wbConnectorPreview, wbSkillCatalog, wbSkillDetail, wbSkillInstall, wbSkillInstalled, wbSkillUninstall } from "./wb-market.js";
|
|
17
20
|
// T03:多市场注册表(R4)与 codebuddy 插件市场(R5)的宿主侧数据层。
|
|
18
21
|
import { readRegistry, upsertMarketplace } from "./wb-registry.js";
|
|
@@ -20,6 +23,8 @@ import { wbPluginCatalog, wbPluginDetail, wbPluginUpdateCheck } from "./wb-plugi
|
|
|
20
23
|
// T06/Awesome:DSH 社区插件市场(awesome-dsh-plugins)的宿主侧数据层。
|
|
21
24
|
import { wbAwesomeCatalog, wbAwesomeDetail, wbAwesomeReadme, wbAwesomeUpdateCheck } from "./wb-awesome-market.js";
|
|
22
25
|
import { readState as readInstallState, record as recordInstallState, forget as forgetInstallState } from "./wb-install-state.js";
|
|
26
|
+
// 自定义套件(Skill 看板):读 / 整集合覆盖保存,数据层 lib/suites-registry.js。
|
|
27
|
+
import { readSuites as readSkillSuites, saveSuites as writeSkillSuites } from "./suites-registry.js";
|
|
23
28
|
import { applyTokens } from "./wb-auth-rules.js";
|
|
24
29
|
import {
|
|
25
30
|
authorizationOverview as oauthOverview,
|
|
@@ -33,8 +38,8 @@ import {
|
|
|
33
38
|
} from "./mcp-oauth.js";
|
|
34
39
|
import { hasTeamBridge, installExpertSkills, listExpertRecent, listExpertSkills, prepareExpertPersona, readExpertPresets, recordExpertRecent, removeExpertPreset, uninstallExpertSkills, uninstallTeamMemberPersonas, wbExpertCatalog, wbExpertDetail, writeExpertPreset } from "./expert-market.js";
|
|
35
40
|
import { attachPersona, detachPersona, listPersonas, reconcilePersonas } from "./session-persona.js";
|
|
36
|
-
//
|
|
37
|
-
import {
|
|
41
|
+
// clearTextCache 的 import 已随 toolTailor 的「清空缓存」动作一起删掉(见下方 toolTailor 注释)。
|
|
42
|
+
import { scanSessionLedger } from "./tool-ledger.js";
|
|
38
43
|
import { evaluateEnvExpression, jsExpressionToTemplate } from "./workspace-config.js";
|
|
39
44
|
import { canonicalWorkspacePath, installAgentRuntime, listWorkspaceRecords, liveWorkspaceAgentCount, projectConnectionsView, readWorkspaceConfigCached, reconcileWorkspaceConnections, reconcileWorkspaceRestricts, reconnectWorkspaceServer, withWorkspaceWrite, workspaceConnectionStatus, workspaceMountErrorsView, workspaceRestrictErrorView, workspaceScopeErrorsView, workspaceToolSchemas, writeWorkspaceConfigFile } from "./workspace-runtime.js";
|
|
40
45
|
|
|
@@ -1310,12 +1315,12 @@ let McpManagerGateway = (() => {
|
|
|
1310
1315
|
async toolTailor(payload) {
|
|
1311
1316
|
// 只读账本:扫描本机会话日志 + 当前注册表 Schema,见 tool-ledger.js。
|
|
1312
1317
|
// days 由调用方传(7/30/90),缺省 30,服务端钳到 [1,365]。
|
|
1313
|
-
//
|
|
1314
|
-
//
|
|
1315
|
-
//
|
|
1316
|
-
//
|
|
1317
|
-
//
|
|
1318
|
-
|
|
1318
|
+
// 「清空解压文本缓存」这个可选动作已删(2026-10-23):它曾声称「先清后扫,面板立即
|
|
1319
|
+
// 看到占用归零」,实测恰好相反 —— 快照取在扫描之后,同一发调用里日志被重新解压回填,
|
|
1320
|
+
// 前后都是 32 条 / 65.8 MB;而 clearTextCache() 的返回值(clearedBytes)压根没进报告
|
|
1321
|
+
// (textCacheStats() 没这个键),面板只能恒显示「正在清空缓存…」。既然既不回收也不修
|
|
1322
|
+
// 任何事,就不再留这条 RPC 分支。tool-ledger.js 仍导出 clearTextCache 原语:它是将来
|
|
1323
|
+
// 真要改缓存策略(槽位按文件数自适应、或只缓存最近若干会话)时的落点,只是现在无人调用。
|
|
1319
1324
|
return buildToolTailorReport(this.ctx, payload || {});
|
|
1320
1325
|
}
|
|
1321
1326
|
async listWorkspaces() {
|
|
@@ -1561,7 +1566,9 @@ let McpManagerGateway = (() => {
|
|
|
1561
1566
|
async whatsNew() {
|
|
1562
1567
|
// 只回数据,不在宿主判定「该不该弹」:「上次看到的版本」是每台浏览器自己的事实
|
|
1563
1568
|
// (localStorage),宿主没有这个视角;判定在客户端(whatsNewFor)。
|
|
1564
|
-
|
|
1569
|
+
// P1:中英字段一起送(text + textEn / where + whereEn),客户端按 locale 选择。
|
|
1570
|
+
// 宿主不读 locale,因为它也可能是老版本、没有 locale 服务;选择只发生在浏览器侧。
|
|
1571
|
+
return { current: pluginVersion(), entries: attachWhatsNewEnglish(WHATS_NEW) };
|
|
1565
1572
|
}
|
|
1566
1573
|
// ── Skill 看板 ────────────────────────────────────────────────────
|
|
1567
1574
|
// 数据真源是 @deepseek-ai/dsh-skill 的 ctx.skills 注册表,本插件只做投影与只读定位,
|
|
@@ -1618,6 +1625,41 @@ let McpManagerGateway = (() => {
|
|
|
1618
1625
|
else spawn(process.platform === "darwin" ? "open" : "xdg-open", [path], { detached: true, stdio: "ignore" }).unref();
|
|
1619
1626
|
return { note: "已交给系统打开" };
|
|
1620
1627
|
}
|
|
1628
|
+
// 卸载一条已安装 skill:永久删除用户级/项目级 skills 根目录内的条目。
|
|
1629
|
+
// 只信注册表给的 source/path(客户端传的路径不参与判断);bundled / runtime / custom
|
|
1630
|
+
// 由数据层拒绝并给出原因。删除后宿主注册表可能缓存了目录扫描结果——调用方
|
|
1631
|
+
// (SkillBoard)先本地收敛列表再 listSkills 刷新,不把「没立刻消失」当成失败。
|
|
1632
|
+
async removeSkill(payload) {
|
|
1633
|
+
const name = String(payload?.name || "").trim();
|
|
1634
|
+
if (!name) throw new Error("缺少 skill 名称");
|
|
1635
|
+
const result = await withSkillScope(this.ctx, payload, async (registry, options) => {
|
|
1636
|
+
if (typeof registry.get !== "function") throw new Error("宿主没有挂载 skill 注册表(@deepseek-ai/dsh-skill),无法卸载");
|
|
1637
|
+
const skill = await registry.get(name, options);
|
|
1638
|
+
if (!skill) throw new Error("没有找到这个 skill(可能刚被移动或删除):" + name);
|
|
1639
|
+
return removeSkillFromDisk(skill, { cwd: options.cwd, home: homedir() });
|
|
1640
|
+
});
|
|
1641
|
+
if (result === undefined) throw new Error("宿主没有挂载 skill 注册表(@deepseek-ai/dsh-skill),无法卸载");
|
|
1642
|
+
return { note: `已卸载 ${name}(${result.kind === "symlink" ? "符号链接" : "目录"}:${result.path})` };
|
|
1643
|
+
}
|
|
1644
|
+
// ── 自定义套件(Skill 看板)─────────────────────────────────────────
|
|
1645
|
+
// 数据层在 lib/suites-registry.js(~/.dsh/armory/suites.json);这里只做 RPC 转发。
|
|
1646
|
+
// listSkillSuites —— 永不抛错:文件缺失 / JSON 损坏 / 结构不对都回落空集合,
|
|
1647
|
+
// 坏文件不该让整个 Skill 看板打不开。
|
|
1648
|
+
// saveSkillSuites —— 整集合覆盖(UI 的「保存」一次提交右侧全部套件)。
|
|
1649
|
+
// payload 形状不合格一律**抛错**,绝不回落成空数组:那等于把用户攒的套件全删了。
|
|
1650
|
+
// 脏数据(无名 / 空成员 / id 重复 / 超上限)由数据层拒绝且不写盘,错误原样冒到面板。
|
|
1651
|
+
// 不加 Remote 装饰器:与 listSkills / expertAttach 同一形态——strict 声明即可达,
|
|
1652
|
+
// 装饰器只是 SRC fallback 的 marker(方向说明见 test/contract-consistency.mjs 顶部)。
|
|
1653
|
+
async listSkillSuites() {
|
|
1654
|
+
return readSkillSuites();
|
|
1655
|
+
}
|
|
1656
|
+
async saveSkillSuites(payload) {
|
|
1657
|
+
const suites = Array.isArray(payload)
|
|
1658
|
+
? payload
|
|
1659
|
+
: (payload && typeof payload === "object" && Array.isArray(payload.suites) ? payload.suites : null);
|
|
1660
|
+
if (suites === null) throw new Error("缺少套件集合:payload.suites 必须是数组");
|
|
1661
|
+
return writeSkillSuites(suites);
|
|
1662
|
+
}
|
|
1621
1663
|
// ── WorkBuddy 市场直连(连接器 MCP + SkillHub 技能)─────────────────
|
|
1622
1664
|
// 数据与安装逻辑都在 lib/wb-market.js;这里只做 RPC 转发与作用域相关的写盘收尾。
|
|
1623
1665
|
// 连接器安装复用 applyManagedImport / applyWorkspaceImport:准入、同名跳过、entry id
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
// en-US overlay for the generated built-in MCP catalog (lib/mcp-catalog.js).
|
|
2
|
+
//
|
|
3
|
+
// Why this file exists: `mcp-catalog.js` is generated from the upstream verified
|
|
4
|
+
// list (see its header) and is not a UI string table, so its Chinese `summary`,
|
|
5
|
+
// `access`, `authNote`, and category labels cannot go through client `t()`
|
|
6
|
+
// (the i18n dictionary is checked bidirectionally against client.js literals and
|
|
7
|
+
// would report every catalog key as an orphan). The host instead ships both
|
|
8
|
+
// languages with the `builtins` RPC, and the client selects by locale.
|
|
9
|
+
//
|
|
10
|
+
// Maintenance: when a catalog entry is added/removed upstream, the parity test
|
|
11
|
+
// (`test/mcp-catalog-i18n.mjs`) fails until this overlay is updated. Keep the
|
|
12
|
+
// English fields free of CJK so an English panel never leaks Chinese here.
|
|
13
|
+
// Proper nouns (Exa, GitHub, ClickHouse, ...) stay as-is.
|
|
14
|
+
export const CATEGORY_LABELS_EN = {
|
|
15
|
+
search: 'Search',
|
|
16
|
+
dev: 'Developer Tools',
|
|
17
|
+
security: 'Security & Reverse Engineering',
|
|
18
|
+
data: 'Databases & Data',
|
|
19
|
+
ai: 'AI & Knowledge',
|
|
20
|
+
browser: 'Browser Automation',
|
|
21
|
+
cloud: 'Cloud & Infrastructure',
|
|
22
|
+
productivity: 'Productivity',
|
|
23
|
+
files: 'Files & Local',
|
|
24
|
+
comms: 'Communication',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Only display labels containing Chinese need an override; ASCII brand names are
|
|
28
|
+
// already language-neutral. `localizeBuiltinItem` falls back to `label`.
|
|
29
|
+
export const CATALOG_LABELS_EN = {
|
|
30
|
+
'deepwiki-fetch': 'DeepWiki (local fetch)',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const MCP_CATALOG_EN = {
|
|
34
|
+
exa: {
|
|
35
|
+
summary: 'Web search and content reading',
|
|
36
|
+
access: 'Usable without a key (rate-limited); add x-api-key to headers to raise the quota',
|
|
37
|
+
authNote: 'The keyless signal is an empty x-api-key header (rate limit measured in testing); set EXA_API_KEY for a higher quota',
|
|
38
|
+
},
|
|
39
|
+
tavily: {
|
|
40
|
+
summary: 'AI-optimized search and web extraction',
|
|
41
|
+
access: 'Free keyless mode has rate limits (responses are identical with a key); the official free key gives 1,000 credits/month, no credit card required',
|
|
42
|
+
},
|
|
43
|
+
firecrawl: {
|
|
44
|
+
summary: 'Web scraping, search, and document parsing',
|
|
45
|
+
access: 'Usable without a key (daily limit); set an API key for a higher quota. For sites requiring browser login, use /v2/mcp-oauth',
|
|
46
|
+
authNote: 'Sends an empty Bearer token in keyless mode; set FIRECRAWL_API_KEY for keyed mode',
|
|
47
|
+
},
|
|
48
|
+
'brave-search': {
|
|
49
|
+
summary: 'Independent web and local search index',
|
|
50
|
+
access: 'Requires your own Brave Search API key (free tier available; paid beyond it)',
|
|
51
|
+
authNote: 'BRAVE_API_KEY is required; there is no keyless path',
|
|
52
|
+
},
|
|
53
|
+
perplexity: {
|
|
54
|
+
summary: 'Real-time web search and deep research',
|
|
55
|
+
access: 'Requires a paid Perplexity API key (Bearer); no free quota',
|
|
56
|
+
authNote: 'PERPLEXITY_API_KEY is required (Bearer); no free quota',
|
|
57
|
+
},
|
|
58
|
+
'google-maps': {
|
|
59
|
+
summary: 'Google Maps platform developer documentation search',
|
|
60
|
+
access: 'Free during the experimental phase, no authentication required (testing returned 2 documentation-search tools from tools/list)',
|
|
61
|
+
},
|
|
62
|
+
fetch: {
|
|
63
|
+
summary: 'Fetch web pages and convert them to LLM-friendly text',
|
|
64
|
+
access: 'Open source and free; no key required',
|
|
65
|
+
},
|
|
66
|
+
github: {
|
|
67
|
+
summary: 'Repositories, issues, pull requests, and code search',
|
|
68
|
+
access: 'Requires a GitHub account with OAuth authorization; a PAT also works. Available features depend on your plan',
|
|
69
|
+
authNote: 'Skip OAuth: configure GITHUB_PERSONAL_ACCESS_TOKEN (a PAT) and connect directly with Bearer',
|
|
70
|
+
},
|
|
71
|
+
gitlab: {
|
|
72
|
+
summary: 'Projects, issues, and merge request access',
|
|
73
|
+
access: 'Available on Free and higher plans; OAuth dynamic registration, including self-hosted instances | WARNING: static token auth is not officially confirmed (official MCP docs only describe OAuth/DCR; issue 586184 requests PAT support and was answered as not on the near-term roadmap), so authorization may fail in DSH',
|
|
74
|
+
authNote: 'Only OAuth dynamic registration is supported; static tokens are not officially supported, so this host will stall during authorization and stay disconnected after installation',
|
|
75
|
+
},
|
|
76
|
+
sentry: {
|
|
77
|
+
summary: 'Error search, performance analysis, and issue triage',
|
|
78
|
+
access: 'Requires Sentry account OAuth authorization (free-tier accounts work)',
|
|
79
|
+
authNote: 'Official docs only mention OAuth; if you issue SENTRY_AUTH_TOKEN in Sentry settings, Bearer may work (not guaranteed by the docs)',
|
|
80
|
+
},
|
|
81
|
+
git: {
|
|
82
|
+
summary: 'Read and write local Git repositories',
|
|
83
|
+
access: 'Open source and free; operates only on local repositories',
|
|
84
|
+
},
|
|
85
|
+
serena: {
|
|
86
|
+
summary: 'Symbol-level code search and refactoring',
|
|
87
|
+
access: 'Open source and free (MIT), runs locally; install first with uv tool install -p 3.13 serena-agent',
|
|
88
|
+
},
|
|
89
|
+
everything: {
|
|
90
|
+
summary: 'Official reference implementation for connectivity checks',
|
|
91
|
+
access: 'Open source and free; intended for demos and testing',
|
|
92
|
+
},
|
|
93
|
+
'fast-context': {
|
|
94
|
+
summary: 'Windsurf-powered semantic code search',
|
|
95
|
+
access: 'Open source and free; if Windsurf is installed its key is picked up automatically, otherwise set WINDSURF_API_KEY',
|
|
96
|
+
authNote: 'Key is picked up automatically when Windsurf is installed; otherwise set WINDSURF_API_KEY',
|
|
97
|
+
},
|
|
98
|
+
jshook: {
|
|
99
|
+
summary: 'JS analysis and security research: hooks, CDP debugging, deobfuscation',
|
|
100
|
+
access: 'Open source and free (AGPL-3.0), runs locally; requires Node 22.12+. Tool profiles are controlled by MCP_TOOL_PROFILE (search ~3K tokens / workflow / full ~40K+); the default profile is sufficient',
|
|
101
|
+
},
|
|
102
|
+
'ida-pro': {
|
|
103
|
+
summary: 'Drive IDA Pro for reverse engineering',
|
|
104
|
+
access: 'Requires IDA Pro installed locally; the service is fetched on demand, no prior pip install needed',
|
|
105
|
+
},
|
|
106
|
+
'frida-mcp': {
|
|
107
|
+
summary: 'Frida dynamic instrumentation: hook functions and trace calls',
|
|
108
|
+
access: 'Open source and free (MIT), runs locally; requires Frida and the target on this machine. Tested with uvx startup and a successful initialize/tools/list handshake; enumerate_processes returned local processes',
|
|
109
|
+
},
|
|
110
|
+
dbx: {
|
|
111
|
+
summary: 'Multi-database queries (Postgres / MySQL / SQLite, etc.)',
|
|
112
|
+
access: 'Open source and free; native local binary. Data sources are configured in the DBX app',
|
|
113
|
+
},
|
|
114
|
+
postgres: {
|
|
115
|
+
summary: 'Postgres queries, indexes, and health analysis',
|
|
116
|
+
access: 'Open source and free; requires your own database connection string (DATABASE_URI)',
|
|
117
|
+
authNote: 'Set DATABASE_URI (the database connection string, including the password)',
|
|
118
|
+
},
|
|
119
|
+
sqlite: {
|
|
120
|
+
summary: 'Local SQLite queries and data analysis',
|
|
121
|
+
access: 'Open source and free; operates only on local .db files. Note: the official project is archived and receives maintenance fixes only',
|
|
122
|
+
},
|
|
123
|
+
redis: {
|
|
124
|
+
summary: 'Redis key-value, vector, and stream operations',
|
|
125
|
+
access: 'Open source and free (MIT); requires your own Redis instance',
|
|
126
|
+
authNote: 'Set REDIS_HOST / REDIS_PORT; include the password if the instance requires one',
|
|
127
|
+
},
|
|
128
|
+
clickhouse: {
|
|
129
|
+
summary: 'ClickHouse queries and schema exploration',
|
|
130
|
+
access: 'Open source and free, read-only by default; requires your own ClickHouse connection details',
|
|
131
|
+
authNote: 'Set CLICKHOUSE_HOST / PORT / USER and CLICKHOUSE_PASSWORD',
|
|
132
|
+
},
|
|
133
|
+
mongodb: {
|
|
134
|
+
summary: 'MongoDB and Atlas data access',
|
|
135
|
+
access: 'Open source and free (Apache-2.0); requires your own connection string',
|
|
136
|
+
authNote: 'Set MDB_MCP_CONNECTION_STRING (a connection string including the password)',
|
|
137
|
+
},
|
|
138
|
+
supabase: {
|
|
139
|
+
summary: 'Supabase project and database management',
|
|
140
|
+
access: 'Requires Supabase account OAuth authorization; parameters such as ?read_only=true can restrict permissions',
|
|
141
|
+
authNote: 'Official docs specify OAuth; if you have SUPABASE_ACCESS_TOKEN, Bearer may work (not guaranteed by the docs)',
|
|
142
|
+
},
|
|
143
|
+
neon: {
|
|
144
|
+
summary: 'Neon project management and queries',
|
|
145
|
+
access: 'Requires Neon account OAuth or an API key; optional read-only mode and single-project scoping',
|
|
146
|
+
authNote: 'Official recommendation is OAuth; set NEON_API_KEY as a Bearer token to connect directly',
|
|
147
|
+
},
|
|
148
|
+
'deepwiki-fetch': {
|
|
149
|
+
summary: 'Fetch DeepWiki pages and convert them to Markdown',
|
|
150
|
+
access: 'Completely free, no authentication; fetches locally and overlaps with the official remote version, so pick one as needed',
|
|
151
|
+
},
|
|
152
|
+
memory: {
|
|
153
|
+
summary: 'Persistent memory backed by a knowledge graph',
|
|
154
|
+
access: 'Open source and free; data is stored in local files',
|
|
155
|
+
},
|
|
156
|
+
'sequential-thinking': {
|
|
157
|
+
summary: 'Step-by-step reasoning and dynamic problem decomposition',
|
|
158
|
+
access: 'Open source and free; no key required',
|
|
159
|
+
},
|
|
160
|
+
context7: {
|
|
161
|
+
summary: 'Real-time documentation for the latest library versions',
|
|
162
|
+
access: 'Usable without a key; a free API key is available on the website to raise the rate limit',
|
|
163
|
+
},
|
|
164
|
+
deepwiki: {
|
|
165
|
+
summary: 'Repository documentation and Q&A',
|
|
166
|
+
access: 'Completely free, no authentication; accesses public repositories only',
|
|
167
|
+
},
|
|
168
|
+
huggingface: {
|
|
169
|
+
summary: 'Model, dataset, and Spaces search',
|
|
170
|
+
access: 'Basic tools work without a key; sign in to use your configured personal Apps and tools',
|
|
171
|
+
},
|
|
172
|
+
arxiv: {
|
|
173
|
+
summary: 'Paper search and full-text access',
|
|
174
|
+
access: 'Open source and free; no key required',
|
|
175
|
+
},
|
|
176
|
+
'chrome-devtools': {
|
|
177
|
+
summary: 'Drive a real Chrome instance for debugging and performance analysis',
|
|
178
|
+
access: 'Open source and free, runs locally; requires Chrome installed',
|
|
179
|
+
},
|
|
180
|
+
playwright: {
|
|
181
|
+
summary: 'Browser automation based on accessibility snapshots',
|
|
182
|
+
access: 'Open source and free (Apache-2.0), runs locally; requires Node.js 18+',
|
|
183
|
+
},
|
|
184
|
+
'cloudflare-api': {
|
|
185
|
+
summary: 'Call the full Cloudflare API',
|
|
186
|
+
access: 'Requires Cloudflare account OAuth authorization; an API token can also be used as a Bearer token',
|
|
187
|
+
authNote: 'Official recommendation is OAuth; set CLOUDFLARE_API_TOKEN as a Bearer token to connect directly',
|
|
188
|
+
},
|
|
189
|
+
'cloudflare-docs': {
|
|
190
|
+
summary: 'Semantic search over official Cloudflare documentation',
|
|
191
|
+
access: 'Usable without a key; no account required',
|
|
192
|
+
},
|
|
193
|
+
aws: {
|
|
194
|
+
summary: 'Access AWS services through mcp-proxy-for-aws',
|
|
195
|
+
access: 'Open source and free; credentials come from the local AWS configuration (~/.aws) and Bedrock access is required',
|
|
196
|
+
authNote: 'Credentials come from local ~/.aws, so no key is needed here; the account must have Bedrock access',
|
|
197
|
+
},
|
|
198
|
+
kubernetes: {
|
|
199
|
+
summary: 'Cluster resources, pods, and Helm management',
|
|
200
|
+
access: 'Open source and free (Apache-2.0); uses the local kubeconfig',
|
|
201
|
+
authNote: 'Uses local kubeconfig credentials, so no key is needed here; the cluster must be reachable',
|
|
202
|
+
},
|
|
203
|
+
docker: {
|
|
204
|
+
summary: 'Run MCP servers uniformly in isolated containers',
|
|
205
|
+
access: 'Open source and free (MIT); requires Docker Desktop or the docker CLI plugin',
|
|
206
|
+
},
|
|
207
|
+
netlify: {
|
|
208
|
+
summary: 'Site builds, deployments, and the Netlify CLI',
|
|
209
|
+
access: 'Requires Netlify account OAuth authorization | WARNING: static token auth is not officially confirmed (NETLIFY_PERSONAL_ACCESS_TOKEN in the official docs is an environment variable for the local stdio package; the remote endpoint only documents OAuth), so authorization may fail in DSH',
|
|
210
|
+
authNote: 'The remote endpoint only documents OAuth; static tokens are not officially confirmed, so this host will stall during authorization and stay disconnected after installation',
|
|
211
|
+
},
|
|
212
|
+
linear: {
|
|
213
|
+
summary: 'Issue, project, and comment management',
|
|
214
|
+
access: 'Requires Linear account OAuth; read-only access is available at https://mcp.linear.app/mcp/readonly',
|
|
215
|
+
authNote: 'Official docs specify OAuth; LINEAR_API_KEY as a Bearer token is not documented, so it will most likely fail to connect after installation',
|
|
216
|
+
},
|
|
217
|
+
atlassian: {
|
|
218
|
+
summary: 'Jira and Confluence read/write',
|
|
219
|
+
access: 'Free tier available (no credit card); requires Atlassian account OAuth authorization',
|
|
220
|
+
authNote: 'Official docs specify OAuth; ATLASSIAN_API_TOKEN as a Bearer token is not documented, so it will most likely fail to connect after installation',
|
|
221
|
+
},
|
|
222
|
+
airtable: {
|
|
223
|
+
summary: 'Read and write Airtable bases and records',
|
|
224
|
+
access: 'Available on all plans (including free); requires Airtable OAuth authorization',
|
|
225
|
+
authNote: 'Official docs specify OAuth; AIRTABLE_TOKEN as a Bearer token is not documented, so it will most likely fail to connect after installation',
|
|
226
|
+
},
|
|
227
|
+
'microsoft-learn': {
|
|
228
|
+
summary: 'Search official Microsoft documentation and code samples',
|
|
229
|
+
access: 'Completely free; no authentication or account required',
|
|
230
|
+
},
|
|
231
|
+
time: {
|
|
232
|
+
summary: 'Time queries and timezone conversion',
|
|
233
|
+
access: 'Open source and free; purely local computation',
|
|
234
|
+
},
|
|
235
|
+
filesystem: {
|
|
236
|
+
summary: 'File read/write within allowed directories',
|
|
237
|
+
access: 'Open source and free; can access only directories explicitly allowed on the command line',
|
|
238
|
+
},
|
|
239
|
+
slack: {
|
|
240
|
+
summary: 'Search messages, send messages, and manage canvases',
|
|
241
|
+
access: 'Create your own Slack app and complete one OAuth flow to obtain an xoxp- user token, then configure it here; the token can be reused long-term',
|
|
242
|
+
authNote: 'Create a Slack app and complete one OAuth flow to obtain an xoxp- user token; set SLACK_USER_TOKEN to reuse it long-term',
|
|
243
|
+
},
|
|
244
|
+
};
|
package/lib/mcp-config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { parseDocument, Scalar, YAMLMap, YAMLSeq, isMap, isSeq } from 'yaml'
|
|
2
2
|
import { isSafeExpression, makeEnvExpression, makeExpression } from './env-expression.js'
|
|
3
3
|
import { MCP_CATALOG, CATEGORY_ORDER, CATEGORY_LABELS, catalogById } from './mcp-catalog.js'
|
|
4
|
+
import { MCP_CATALOG_EN, CATEGORY_LABELS_EN, CATALOG_LABELS_EN } from './mcp-catalog-en.js'
|
|
4
5
|
|
|
5
6
|
export const MCP_PLUGIN_NAME = '@deepseek-ai/dsh-mcp-client'
|
|
6
7
|
const JS_TAG = 'tag:yaml.org,2002:js'
|
|
@@ -21,15 +22,24 @@ const UNSUPPORTED_TEMPLATE = '不支持的变量表达式(只允许 ${VAR}、$
|
|
|
21
22
|
// `name`(写进配置的 serverName)取 id,是刻意的:id 一旦发布就不能改,而 serverName
|
|
22
23
|
// 决定工具名 `mcp__<serverName>__<tool>`,让两者同源就不会出现「目录改名导致老配置变孤儿」。
|
|
23
24
|
export const BUILTIN_MCP_SERVERS = MCP_CATALOG.map((entry) => {
|
|
25
|
+
// P1:英文覆盖与生成目录分开维护(lib/mcp-catalog-en.js)。生成器没有随本仓库
|
|
26
|
+
// 分发,直接改 mcp-catalog.js 会在下次生成时被冲掉;覆盖表缺失时逐字段回落中文。
|
|
27
|
+
const en = MCP_CATALOG_EN[entry.id] || {}
|
|
24
28
|
const server = {
|
|
25
29
|
id: entry.id,
|
|
26
30
|
label: entry.label,
|
|
31
|
+
labelEn: CATALOG_LABELS_EN[entry.id] || entry.label,
|
|
27
32
|
summary: entry.summary,
|
|
33
|
+
summaryEn: en.summary || entry.summary,
|
|
28
34
|
access: entry.access,
|
|
35
|
+
accessEn: en.access || entry.access,
|
|
29
36
|
// OPT-24:`auth` 是给面板判断「当前宿主装不装得上」的展示元数据,不是配置字段——
|
|
30
37
|
// `builtinSpec()` 只挑 MANAGED_CONFIG_FIELDS、`makeEntry()` 只挑固定字段表,两者都
|
|
31
38
|
// 不会带它,所以它和 authNote 落不进用户配置文件,mcp-client 收不到不认识的键。
|
|
32
39
|
auth: entry.auth,
|
|
40
|
+
// authNoteEn 与 authNote 同生同灭:`none` 条目没有说明,补空串会让面板分不清
|
|
41
|
+
// 「没有说明」和「说明是空的」。
|
|
42
|
+
...(entry.authNote !== undefined ? { authNoteEn: en.authNote || entry.authNote } : {}),
|
|
33
43
|
name: entry.id,
|
|
34
44
|
transport: entry.transport,
|
|
35
45
|
}
|
|
@@ -554,13 +564,17 @@ export function builtinMcpCatalog(effectiveServers) {
|
|
|
554
564
|
return {
|
|
555
565
|
id: builtin.id,
|
|
556
566
|
label: builtin.label,
|
|
567
|
+
labelEn: builtin.labelEn || builtin.label,
|
|
557
568
|
summary: builtin.summary,
|
|
569
|
+
summaryEn: builtin.summaryEn || builtin.summary,
|
|
558
570
|
access: builtin.access,
|
|
571
|
+
accessEn: builtin.accessEn || builtin.access,
|
|
559
572
|
// OPT-24:`auth` 走 builtin(BUILTIN_MCP_SERVERS 已投影),面板据此降级提示
|
|
560
573
|
// oauth 条目;`authNote` 只在存在时挂键——`none` 条目根本没有这个字段,补成空串
|
|
561
574
|
// 会让面板分不清「没有说明」和「说明是空的」。
|
|
562
575
|
auth: builtin.auth,
|
|
563
576
|
...(builtin.authNote !== undefined ? { authNote: builtin.authNote } : {}),
|
|
577
|
+
...(builtin.authNoteEn !== undefined ? { authNoteEn: builtin.authNoteEn } : {}),
|
|
564
578
|
name: builtin.name,
|
|
565
579
|
transport: builtin.transport,
|
|
566
580
|
configuration: builtinSpec(builtin),
|
|
@@ -569,6 +583,7 @@ export function builtinMcpCatalog(effectiveServers) {
|
|
|
569
583
|
// —— 市场展示字段(bundle 侧读不到常量,只能这里带过去)——
|
|
570
584
|
category: source.category,
|
|
571
585
|
categoryLabel: MCP_CATEGORY_LABELS[source.category],
|
|
586
|
+
categoryLabelEn: CATEGORY_LABELS_EN[source.category] || MCP_CATEGORY_LABELS[source.category],
|
|
572
587
|
vendor: source.vendor,
|
|
573
588
|
homepage: source.homepage,
|
|
574
589
|
envKeys: source.envKeys || [],
|
package/lib/mcp-oauth.js
CHANGED
|
@@ -29,7 +29,7 @@ import { homedir } from "node:os";
|
|
|
29
29
|
import { join } from "node:path";
|
|
30
30
|
|
|
31
31
|
/** 插件版本:DCR 的 clientInfo 与 UA 共用一处,避免两边各自漂移。 */
|
|
32
|
-
const PLUGIN_VERSION = "2.1
|
|
32
|
+
const PLUGIN_VERSION = "2.2.1";
|
|
33
33
|
const USER_AGENT = `dsh-armory/${PLUGIN_VERSION} (+mcp-oauth)`;
|
|
34
34
|
const NET_TIMEOUT_MS = 15_000;
|
|
35
35
|
/** 授权流程整体存活时间:超过就判过期,避免会话表无限增长。 */
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// 已安装 skill 的卸载数据层(Skill 看板「卸载」的唯一落盘点)。
|
|
2
|
+
//
|
|
3
|
+
// 为什么不让客户端传路径直接删:path 来自 listSkills 的回包,但 RPC 入参是浏览器可写的。
|
|
4
|
+
// 这里只认「宿主注册表给这条 skill 的真实 source + path」,并且把删除范围钉死在四类
|
|
5
|
+
// skills 根目录里——用户级 ~/.agents/skills、~/.dsh/skills,项目级 <cwd>/.agents/skills、
|
|
6
|
+
// <cwd>/.dsh/skills。随 DSH 分发(bundled)、运行时注册(runtime)、自定义目录(custom)
|
|
7
|
+
// 一律拒绝:前两者不该由用户面板删,后者没有可信的根目录可用来做包含性校验。
|
|
8
|
+
//
|
|
9
|
+
// 安全闸门(按顺序,任一不过即抛错,绝不"尽力而为"地删):
|
|
10
|
+
// 1. source 在白名单内,且能推导出绝对根目录(项目级必须有 cwd);
|
|
11
|
+
// 2. path 是绝对路径,且 resolve 后严格位于根目录内(≠ 根自身,`..` 穿越不过);
|
|
12
|
+
// 宿主注册表返回的 path 通常指向 `<skill>/SKILL.md`,先归一化到其父目录再校验,
|
|
13
|
+
// 否则会把 SKILL.md 当成「不是目录」误拒(GUI 验证抓到的真实形态);
|
|
14
|
+
// 3. 父目录的 realpath 必须仍在根目录 realpath 内(防祖先符号链接把写入面带到别处);
|
|
15
|
+
// 4. 目标是符号链接 → 只 unlink 链接本身,**绝不**触碰链接指向的目录;
|
|
16
|
+
// 5. 目标是目录 → 其 realpath 仍在根内,且目录里确实有 SKILL.md(文件),才递归删除。
|
|
17
|
+
//
|
|
18
|
+
// 删除是**永久删除**(不进回收站):UI 侧有二次确认,并明示不可撤销。这与用户对删除的
|
|
19
|
+
// 偏好一致;不做「移到 .trash」这类软删除——那会让看板显示已卸载、磁盘却仍在。
|
|
20
|
+
//
|
|
21
|
+
// 纯数据层:零网络、零宿主依赖;home / cwd 可注入,测试用 mkdtemp 隔离。
|
|
22
|
+
import { lstat, realpath, rm, stat, unlink } from "node:fs/promises";
|
|
23
|
+
import { homedir } from "node:os";
|
|
24
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
25
|
+
|
|
26
|
+
/** 可卸载来源:值与宿主注册表的 skill.source 一致,客户端 SKILL_REMOVABLE_SOURCES 必须同步。 */
|
|
27
|
+
export const REMOVABLE_SKILL_SOURCES = ["user-agents", "user-dsh", "project-agents", "project-dsh"];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 来源 → skills 根目录。project-* 需要 cwd;拿不到时返回 null(拒绝,而不是猜一个目录)。
|
|
31
|
+
* 只写「从哪来」,不校验存在性——存在性在删除流程里按目标路径判断。
|
|
32
|
+
*/
|
|
33
|
+
export function skillRootFor(source, options = {}) {
|
|
34
|
+
const home = options.home || homedir();
|
|
35
|
+
const cwd = options.cwd;
|
|
36
|
+
switch (source) {
|
|
37
|
+
case "user-agents": return join(home, ".agents", "skills");
|
|
38
|
+
case "user-dsh": return join(home, ".dsh", "skills");
|
|
39
|
+
case "project-agents": return cwd ? join(cwd, ".agents", "skills") : null;
|
|
40
|
+
case "project-dsh": return cwd ? join(cwd, ".dsh", "skills") : null;
|
|
41
|
+
default: return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 严格包含:child 在 parent 之内且不等于 parent。 */
|
|
46
|
+
function isInside(parent, child) {
|
|
47
|
+
const rel = relative(parent, child);
|
|
48
|
+
return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 包含或相等:符号链接项只做父目录校验,父目录可以就是根。 */
|
|
52
|
+
function isInsideOrEqual(parent, child) {
|
|
53
|
+
const rel = relative(parent, child);
|
|
54
|
+
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function describeSource(source) {
|
|
58
|
+
return source ? `「${source}」` : "未知来源";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 从磁盘永久删除一条已发现的 skill。
|
|
63
|
+
*
|
|
64
|
+
* @param {{ name?: string, source?: string, path?: string }} skill 宿主注册表返回的条目(path 可为目录或目录内的 SKILL.md)
|
|
65
|
+
* @param {{ cwd?: string, home?: string }} [options]
|
|
66
|
+
* @returns {Promise<{ path: string, kind: "symlink" | "dir" }>}
|
|
67
|
+
*/
|
|
68
|
+
export async function removeSkillFromDisk(skill, options = {}) {
|
|
69
|
+
const source = typeof skill?.source === "string" ? skill.source : "";
|
|
70
|
+
const root = skillRootFor(source, options);
|
|
71
|
+
if (!root) {
|
|
72
|
+
if (source === "project-agents" || source === "project-dsh") {
|
|
73
|
+
throw new Error(`卸载项目级 skill 需要当前项目路径(来源:${describeSource(source)})`);
|
|
74
|
+
}
|
|
75
|
+
throw new Error(`这个 skill 来自${describeSource(source)},不在可卸载范围内(只支持用户级/项目级的 .dsh/skills 与 .agents/skills)`);
|
|
76
|
+
}
|
|
77
|
+
if (!isAbsolute(root)) throw new Error("拒绝卸载:无法确定 skills 根目录的绝对路径");
|
|
78
|
+
const raw = typeof skill?.path === "string" ? skill.path.trim() : "";
|
|
79
|
+
if (!raw) throw new Error("这个 skill 没有可卸载的磁盘路径(运行时注册的 skill 不落盘)");
|
|
80
|
+
if (!isAbsolute(raw)) throw new Error("拒绝卸载:skill 路径不是绝对路径");
|
|
81
|
+
|
|
82
|
+
const rawAbs = resolve(raw);
|
|
83
|
+
// 注册表把 skill.path 指向清单文件(<skill>/SKILL.md),删除目标是它所在的目录。
|
|
84
|
+
// 若直接拿清单路径去删,只会得到「该路径不是目录」——真实 GUI 首跑就撞上了。
|
|
85
|
+
const target = basename(rawAbs).toLowerCase() === "skill.md" ? dirname(rawAbs) : rawAbs;
|
|
86
|
+
const rootAbs = resolve(root);
|
|
87
|
+
if (!isInside(rootAbs, target)) {
|
|
88
|
+
throw new Error(`拒绝卸载:skill 路径不在预期的 skills 根目录内(${rootAbs})`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let info;
|
|
92
|
+
try {
|
|
93
|
+
info = await lstat(target);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (error?.code === "ENOENT") throw new Error("这个 skill 已经不在磁盘上了(可能刚被移动或删除)");
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 父目录的 realpath 校验:防「根内路径 → 祖先符号链接实际指向根外」。根目录本身是
|
|
100
|
+
// 符号链接(例如 ~/.agents → 别处)是允许的,所以比的是 realpath 后的根。
|
|
101
|
+
const rootReal = await realpath(rootAbs);
|
|
102
|
+
const parentReal = await realpath(dirname(target));
|
|
103
|
+
if (!isInsideOrEqual(rootReal, parentReal)) {
|
|
104
|
+
throw new Error("拒绝卸载:skill 路径经符号链接指向了 skills 根目录之外");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (info.isSymbolicLink()) {
|
|
108
|
+
// 只删链接本身。链接可能指向任意位置(例如 tabbit 这种外部安装的 skill),
|
|
109
|
+
// rm/lstat 都不会跟随,但我们仍显式只用 unlink,语义一目了然。
|
|
110
|
+
await unlink(target);
|
|
111
|
+
return { path: target, kind: "symlink" };
|
|
112
|
+
}
|
|
113
|
+
if (!info.isDirectory()) throw new Error("拒绝卸载:该路径不是目录(也不是符号链接)");
|
|
114
|
+
|
|
115
|
+
const targetReal = await realpath(target);
|
|
116
|
+
if (!isInside(rootReal, targetReal)) {
|
|
117
|
+
throw new Error("拒绝卸载:skill 目录经符号链接指向了 skills 根目录之外");
|
|
118
|
+
}
|
|
119
|
+
const manifest = await stat(join(target, "SKILL.md")).catch(() => null);
|
|
120
|
+
if (!manifest || !manifest.isFile()) {
|
|
121
|
+
throw new Error("拒绝卸载:目录里没有 SKILL.md,看起来不是 skill 目录");
|
|
122
|
+
}
|
|
123
|
+
await rm(target, { recursive: true, force: false });
|
|
124
|
+
return { path: target, kind: "dir" };
|
|
125
|
+
}
|