@max-null/dsh-skill-mcp-center 0.4.1 → 0.5.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 +27 -5
- package/client.js +70 -17
- package/dist/frontmatter.js +4 -2
- package/dist/service.d.ts +31 -1
- package/dist/service.js +72 -7
- package/docs/shots/sidebar-mcp.png +0 -0
- package/docs/shots/sidebar-skills.png +0 -0
- package/docs/shots/skill-mcp-1.png +0 -0
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -1,16 +1,41 @@
|
|
|
1
1
|
# dsh-skill-mcp-center
|
|
2
2
|
|
|
3
|
+
本插件属于 **`@max-null/*` 插件系列**——这一系列共同构成 **[SSID(思灵 · Seek Soul in Darkness)](https://github.com/Max-Null/seek-soul-in-darkness)** 桌面体验。SSID 是整合它们的盒:`dsh-capture` · `dsh-chat-rail` · `dsh-chinese-thinking` · `dsh-draft-polish` · `dsh-guardian` · `dsh-habit` · `dsh-memory` · `dsh-node-appearance` · `dsh-plugin-center` · `dsh-quick-toolbar` · `dsh-skill-mcp-center` · `dsh-ssid-panels` · `dsh-ssid-zh-ui` · `dsh-achievements`。
|
|
4
|
+
|
|
5
|
+
This plugin belongs to the **`@max-null/*` family** — a set of plugins that together form the **[SSID (思灵 · Seek Soul in Darkness)](https://github.com/Max-Null/seek-soul-in-darkness)** desktop experience.
|
|
6
|
+
|
|
3
7
|
Skill & MCP management center for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (`dsh`) — manage skills and MCP servers in Settings, with live MCP status in the sidebar.
|
|
4
8
|
|
|
5
9
|
Skill 与 MCP 管理中心:在设置里管理 skills 与 MCP 服务器,右侧边栏查看 MCP 实时状态。
|
|
6
10
|
|
|
7
11
|
## Features / 功能
|
|
8
12
|
|
|
9
|
-
- **Skill management / Skill 管理** — browse every skill by tier (system / user / workspace / runtime), toggle model invocation via the `disable-model-invocation` frontmatter (disk-backed skills only).
|
|
13
|
+
- **Skill management / Skill 管理** — browse every skill by tier (system / user / workspace / runtime), toggle model invocation via the `disable-model-invocation` frontmatter (disk-backed skills only). Covers skills shipped **inside plugin packages** (e.g. `@max-null/dsh-skills`), not just the user-level roots — the host-level skill filesystem is disabled in web-app, so a plugin's own `skills/` directory would otherwise be invisible while its skills are loaded and in effect.
|
|
10
14
|
- **MCP management / MCP 管理** — add / edit / remove `mcp-client` servers, enable/disable without deleting config, all **hot-applied** through `ctx.loader` (no restart).
|
|
11
|
-
- **Live status / 实时状态** — a sidebar "MCP" tab (via `dsh-better-sidebar`) showing per-server connection state
|
|
15
|
+
- **Live status / 实时状态** — a sidebar "MCP" tab (via `dsh-better-sidebar`) showing per-server connection state; **click a server row to expand its tools' names and descriptions** (collapsed by default — a server can expose dozens of them). Names come from `tools.schemas()`; the redundant `mcp__<server>__` prefix is hidden while the full name stays on hover. Polled while visible and following the session.
|
|
12
16
|
- **Skin-compatible / 皮肤兼容** — every color uses `var(--dsw-*)` tokens.
|
|
13
17
|
|
|
18
|
+
## 截图
|
|
19
|
+
|
|
20
|
+
### 设置页:集中管理
|
|
21
|
+
|
|
22
|
+
装完后设置里多出「Skill & MCP」一项,可逐条控制各模型对 Skill/MCP 的可见性。**入口:** 设置 → Skill & MCP
|
|
23
|
+
|
|
24
|
+

|
|
25
|
+
|
|
26
|
+
### 侧栏:Skill 一览
|
|
27
|
+
|
|
28
|
+
搜索 + 命名空间筛选(全部 / 全局 / 工作区),每条带开关与 `MD`(查看 SKILL.md)。**插件包内自带的 skill 同样列出**,并标出来源插件——下图里 8 个 `ssid-*` 来自 `dsh-skills`,而用户级的 `ssid-release` 没有来源标签,这正是两者的区别:
|
|
29
|
+
|
|
30
|
+

|
|
31
|
+
|
|
32
|
+
### 侧栏:MCP 状态与工具
|
|
33
|
+
|
|
34
|
+
每个 `mcp-client` 服务器的连接状态与工具数;**点服务器行展开**工具清单(默认收起,避免几十个工具占满面板)。工具名取自工具表、描述取自 `tools.schemas()`,冗余的 `mcp__<server>__` 前缀隐去(全名留在 hover):
|
|
35
|
+
|
|
36
|
+

|
|
37
|
+
|
|
38
|
+
|
|
14
39
|
## Install / 安装
|
|
15
40
|
|
|
16
41
|
```sh
|
|
@@ -34,6 +59,3 @@ pnpm build # tsc (host) + esbuild (browser bundle)
|
|
|
34
59
|
|
|
35
60
|
## SSID 系列
|
|
36
61
|
|
|
37
|
-
本插件属于 **`@max-null/*` 插件系列**——这一系列共同构成 **[SSID(思灵 · Seek Soul in Darkness)](https://github.com/Max-Null/seek-soul-in-darkness)** 桌面体验。SSID 是整合它们的盒:`dsh-chat-rail` · `dsh-chinese-thinking` · `dsh-draft-polish` · `dsh-guardian` · `dsh-habit` · `dsh-header-unify` · `dsh-memory` · `dsh-node-appearance` · `dsh-plugin-center` · `dsh-skill-mcp-center` · `dsh-ssid-panels` · `dsh-ssid-zh-ui`。
|
|
38
|
-
|
|
39
|
-
This plugin belongs to the **`@max-null/*` family** — a set of plugins that together form the **[SSID (思灵 · Seek Soul in Darkness)](https://github.com/Max-Null/seek-soul-in-darkness)** desktop experience.
|
package/client.js
CHANGED
|
@@ -57,7 +57,7 @@ var CSS = `
|
|
|
57
57
|
.smc-meta { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-top: 10px; }
|
|
58
58
|
.smc-spacer { flex: 1; }
|
|
59
59
|
|
|
60
|
-
.smc-badge { display: inline-flex; align-items: center; padding: 1px 8px; border-radius: 999px; font-size: 11px; font-weight: 500; line-height: 17px; white-space: nowrap; background: var(--dsw-alias-bg-module-platform); color: var(--dsw-alias-label-secondary); }
|
|
60
|
+
.smc-badge { display: inline-flex; align-items: center; flex: none; padding: 1px 8px; border-radius: 999px; font-size: 11px; font-weight: 500; line-height: 17px; white-space: nowrap; background: var(--dsw-alias-bg-module-platform); color: var(--dsw-alias-label-secondary); }
|
|
61
61
|
.smc-badge.user { background: var(--dsw-alias-state-business-tertiary); color: var(--dsw-alias-state-business-primary); }
|
|
62
62
|
.smc-badge.workspace { background: var(--dsw-alias-state-success-tertiary); color: var(--dsw-alias-state-success-primary); }
|
|
63
63
|
.smc-badge.bundled { background: var(--dsw-alias-bg-module-platform); color: var(--dsw-alias-label-tertiary); }
|
|
@@ -100,11 +100,17 @@ var CSS = `
|
|
|
100
100
|
.smc-search-sidebar { height: 28px; padding: 0 12px; border-radius: 18px; border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-primary); font-size: 13px; outline: none; width: 100%; font-family: inherit; box-sizing: border-box; }
|
|
101
101
|
.smc-search-sidebar:focus { border-color: var(--dsw-alias-state-business-primary); }
|
|
102
102
|
.smc-search-sidebar::placeholder { color: var(--dsw-alias-label-caption); }
|
|
103
|
-
.smc-srv { display: flex; align-items: center; gap: 8px; padding: 8px 10px; border-radius: 8px; }
|
|
103
|
+
.smc-srv { display: flex; align-items: center; gap: 8px; padding: 8px 10px; border-radius: 8px; width: 100%; box-sizing: border-box; border: none; background: none; font: inherit; color: inherit; text-align: left; cursor: pointer; }
|
|
104
|
+
.smc-srv .smc-chevron { margin: 0 2px 0 4px; }
|
|
104
105
|
.smc-srv:hover { background: var(--dsw-alias-interactive-bg-hover); }
|
|
105
106
|
.smc-srv-name { font-size: 13px; font-weight: 500; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-primary); }
|
|
106
107
|
.smc-srv-desc { font-size: 11px; color: var(--dsw-alias-label-tertiary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin: 0 10px 4px 26px; flex: none; }
|
|
107
108
|
.smc-srv-state { font-size: 11px; color: var(--dsw-alias-label-caption); padding: 0 10px 6px 26px; }
|
|
109
|
+
.smc-tools { list-style: none; margin: 0 0 8px; padding: 0 10px 0 26px; display: flex; flex-direction: column; gap: 0; }
|
|
110
|
+
.smc-tool { display: flex; flex-direction: column; gap: 1px; padding: 5px 0; border-top: 1px solid var(--dsw-alias-border-l1); }
|
|
111
|
+
.smc-tool:first-child { border-top: none; }
|
|
112
|
+
.smc-tool-name { font-size: 11.5px; font-weight: 500; color: var(--dsw-alias-label-secondary); font-family: ui-monospace, 'Cascadia Code', Consolas, monospace; word-break: break-all; }
|
|
113
|
+
.smc-tool-desc { font-size: 11px; line-height: 1.5; color: var(--dsw-alias-label-tertiary); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
|
108
114
|
.smc-empty { padding: 24px; text-align: center; color: var(--dsw-alias-label-tertiary); font-size: 12.5px; }
|
|
109
115
|
.smc-ns-bar { display: flex; gap: 4px; margin-bottom: 8px; }
|
|
110
116
|
.smc-ns-btn { flex: 1; height: 26px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; background: none; color: var(--dsw-alias-label-secondary); font-size: 12px; cursor: pointer; font-family: inherit; }
|
|
@@ -496,7 +502,7 @@ function SkillView() {
|
|
|
496
502
|
" \xB7 ",
|
|
497
503
|
s.provider
|
|
498
504
|
] }),
|
|
499
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-badge", children: s.source }),
|
|
505
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-badge", children: sourceLabel(s.source) }),
|
|
500
506
|
!s.modelInvocable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-badge disabled", children: t("modelDisabled") }),
|
|
501
507
|
!s.writable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-badge disabled", children: t("readOnlyBadge") }),
|
|
502
508
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-spacer" }),
|
|
@@ -721,6 +727,7 @@ function CenterPanel() {
|
|
|
721
727
|
function McpSidebarTab({ visible }) {
|
|
722
728
|
useLocale();
|
|
723
729
|
const [items, setItems] = (0, import_react.useState)([]);
|
|
730
|
+
const [openIds, setOpenIds] = (0, import_react.useState)(() => /* @__PURE__ */ new Set());
|
|
724
731
|
(0, import_react.useEffect)(() => {
|
|
725
732
|
if (!visible) return;
|
|
726
733
|
const tick = () => {
|
|
@@ -735,20 +742,50 @@ function McpSidebarTab({ visible }) {
|
|
|
735
742
|
clearInterval(timer);
|
|
736
743
|
};
|
|
737
744
|
}, [visible]);
|
|
745
|
+
const toggle = (name) => {
|
|
746
|
+
setOpenIds((prev) => {
|
|
747
|
+
const next = new Set(prev);
|
|
748
|
+
if (next.has(name)) next.delete(name);
|
|
749
|
+
else next.add(name);
|
|
750
|
+
return next;
|
|
751
|
+
});
|
|
752
|
+
};
|
|
738
753
|
if (items.length === 0) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-empty", children: t("noMcpServer") });
|
|
739
754
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-sidebar", children: items.map((s) => {
|
|
740
755
|
const dotCls = s.fiberPhase === "failed" ? " failed" : s.connected ? "" : " idle";
|
|
741
756
|
const state = s.fiberPhase === "failed" ? t("failed") : s.connected ? t("connected") : t("notSynced");
|
|
757
|
+
const expandable = s.tools.length > 0;
|
|
758
|
+
const open = expandable && openIds.has(s.serverName);
|
|
742
759
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
|
|
743
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
" tools
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
760
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
761
|
+
"button",
|
|
762
|
+
{
|
|
763
|
+
type: "button",
|
|
764
|
+
className: "smc-srv",
|
|
765
|
+
title: `fiber ${s.fiberPhase ?? "?"} \xB7 ${s.toolCount} tools (${s.statusSource})`,
|
|
766
|
+
"aria-expanded": expandable ? open : void 0,
|
|
767
|
+
onClick: () => {
|
|
768
|
+
if (expandable) toggle(s.serverName);
|
|
769
|
+
},
|
|
770
|
+
children: [
|
|
771
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `smc-dot${dotCls}` }),
|
|
772
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-srv-name", children: s.serverName }),
|
|
773
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "smc-srv-count", children: [
|
|
774
|
+
s.toolCount,
|
|
775
|
+
" tools"
|
|
776
|
+
] }),
|
|
777
|
+
expandable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `smc-chevron${open ? " open" : ""}` })
|
|
778
|
+
]
|
|
779
|
+
}
|
|
780
|
+
),
|
|
781
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-srv-state", children: state }),
|
|
782
|
+
open && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { className: "smc-tools", children: s.tools.map((tool) => {
|
|
783
|
+
const short = tool.name.startsWith(`mcp__${s.serverName}__`) ? tool.name.slice(`mcp__${s.serverName}__`.length) : tool.name;
|
|
784
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { className: "smc-tool", title: tool.name, children: [
|
|
785
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-tool-name", children: short }),
|
|
786
|
+
tool.description !== "" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-tool-desc", children: tool.description })
|
|
787
|
+
] }, tool.name);
|
|
788
|
+
}) })
|
|
752
789
|
] }, s.serverName);
|
|
753
790
|
}) });
|
|
754
791
|
}
|
|
@@ -757,9 +794,17 @@ function Toast() {
|
|
|
757
794
|
if (toast === null) return null;
|
|
758
795
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: `smc-toast ${toast.kind}`, children: toast.message });
|
|
759
796
|
}
|
|
797
|
+
function sourceLabel(source, short = false) {
|
|
798
|
+
if (!source.startsWith("plugin:")) return source;
|
|
799
|
+
const pkg = source.slice("plugin:".length);
|
|
800
|
+
const name = pkg.split("/").pop() ?? pkg;
|
|
801
|
+
return short ? name : `plugin \xB7 ${name}`;
|
|
802
|
+
}
|
|
760
803
|
function skillInNamespace(s, ns) {
|
|
761
804
|
if (ns === "all") return true;
|
|
762
|
-
if (ns === "global")
|
|
805
|
+
if (ns === "global") {
|
|
806
|
+
return s.source === "user-dsh" || s.source === "user-agents" || s.source.startsWith("plugin:");
|
|
807
|
+
}
|
|
763
808
|
return s.source === "project-dsh" || s.source === "project-agents";
|
|
764
809
|
}
|
|
765
810
|
function SidebarSkillTab({ visible, cwd }) {
|
|
@@ -840,8 +885,8 @@ function SidebarSkillTab({ visible, cwd }) {
|
|
|
840
885
|
${s.path}`, children: [
|
|
841
886
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `smc-dot${s.modelInvocable ? "" : " idle"}` }),
|
|
842
887
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-srv-name", children: s.name }),
|
|
843
|
-
!s.modelInvocable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-
|
|
844
|
-
!s.writable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-
|
|
888
|
+
!s.modelInvocable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-badge", children: t("modelDisabled") }),
|
|
889
|
+
!s.writable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-badge", children: sourceLabel(s.source, true) }),
|
|
845
890
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
846
891
|
"button",
|
|
847
892
|
{
|
|
@@ -914,7 +959,14 @@ function apply(ctx) {
|
|
|
914
959
|
sidebarCtx.effect(() => service.registerTab({
|
|
915
960
|
id: "@max-null/dsh-skill-mcp-center:mcp",
|
|
916
961
|
title: () => "MCP",
|
|
917
|
-
|
|
962
|
+
// 图标风格对齐 DSH 原生右栏:彩色线性、22px(「开始」页的图标容器
|
|
963
|
+
// .geFEbW_entryIcon 是 26px、自带占位图标 22px,整列基准即 22 ——
|
|
964
|
+
// 2026-09-14 用户指「尺寸 14 不太对,其他都在 22 左右」)。
|
|
965
|
+
// **必须是返回 ReactNode 的函数**:DSH 原生右侧栏只认函数形式,直接传 JSX 元素
|
|
966
|
+
// 会被忽略并回落到它的占位图标(`geFEbW_placeholder`,22px 灰圆)——
|
|
967
|
+
// 2026-09-14 实测:同一份 icon 在 better-sidebar 自绘的底部工作台里正常显示,
|
|
968
|
+
// 在原生右栏「开始」页却是占位符,据此定位到形式差异。
|
|
969
|
+
icon: () => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", stroke: "#4d6bfe", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
918
970
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: "2", y: "2", width: "20", height: "8", rx: "2", ry: "2" }),
|
|
919
971
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: "2", y: "14", width: "20", height: "8", rx: "2", ry: "2" }),
|
|
920
972
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "6", y1: "6", x2: "6.01", y2: "6" }),
|
|
@@ -927,7 +979,8 @@ function apply(ctx) {
|
|
|
927
979
|
sidebarCtx.effect(() => service.registerTab({
|
|
928
980
|
id: "@max-null/dsh-skill-mcp-center:skills",
|
|
929
981
|
title: () => "Skill",
|
|
930
|
-
|
|
982
|
+
// 同上:必须是返回 ReactNode 的函数(DSH 原生右栏只认函数形式,传元素会回落占位图标)
|
|
983
|
+
icon: () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", stroke: "#f59e0b", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M13 2 3 14h7l-1 8 10-12h-7l1-8z" }) }),
|
|
931
984
|
order: 71,
|
|
932
985
|
single: true,
|
|
933
986
|
component: (props) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SidebarSkillTab, { visible: props.visible, cwd: props.scope?.cwd })
|
package/dist/frontmatter.js
CHANGED
|
@@ -6,16 +6,18 @@
|
|
|
6
6
|
import { load } from 'js-yaml';
|
|
7
7
|
/** Toggle `disable-model-invocation` in SKILL.md text; creates frontmatter if absent. */
|
|
8
8
|
export function setDisableModelInvocation(text, disabled) {
|
|
9
|
+
const eol = text.includes('\r\n') ? '\r\n' : '\n';
|
|
9
10
|
const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text);
|
|
10
11
|
if (match === null) {
|
|
11
|
-
return
|
|
12
|
+
return `---${eol}disable-model-invocation: ${disabled}${eol}---${eol}${eol}${text}`;
|
|
12
13
|
}
|
|
13
14
|
const full = match[0];
|
|
14
15
|
const body = match[1];
|
|
15
16
|
const line = /^(\s*)disable-model-invocation\s*:.*$/m;
|
|
16
17
|
const next = line.test(body)
|
|
17
18
|
? body.replace(line, `$1disable-model-invocation: ${disabled}`)
|
|
18
|
-
|
|
19
|
+
// 追加分支沿用文件现有行尾(CRLF 文件不混入 LF,防止混合行尾扩散)。
|
|
20
|
+
: body.replace(/\s*$/, `${eol}disable-model-invocation: ${disabled}`);
|
|
19
21
|
return text.replace(full, full.replace(body, next));
|
|
20
22
|
}
|
|
21
23
|
/** Parse a SKILL.md frontmatter block; null when absent or missing a name. */
|
package/dist/service.d.ts
CHANGED
|
@@ -44,11 +44,23 @@ export interface McpServer {
|
|
|
44
44
|
disabled: boolean;
|
|
45
45
|
fiberPhase: string | null;
|
|
46
46
|
}
|
|
47
|
+
/** One tool a server contributes, as the management surface lists it. */
|
|
48
|
+
export interface McpToolView {
|
|
49
|
+
name: string;
|
|
50
|
+
description: string;
|
|
51
|
+
}
|
|
47
52
|
/** Runtime status of one MCP server (sidebar polling). */
|
|
48
53
|
export interface McpServerStatus {
|
|
49
54
|
serverName: string;
|
|
50
55
|
fiberPhase: string | null;
|
|
51
56
|
toolCount: number;
|
|
57
|
+
/**
|
|
58
|
+
* The tools themselves. A bare count says nothing about what the server
|
|
59
|
+
* actually offers, so the surface lists names and descriptions too
|
|
60
|
+
* (2026-09-14 用户:MCP 的 tools 信息太少了,最好有名称和介绍)。
|
|
61
|
+
* Names come from `tools.schemas()`, which whitelists name/description.
|
|
62
|
+
*/
|
|
63
|
+
tools: McpToolView[];
|
|
52
64
|
connected: boolean;
|
|
53
65
|
statusSource: 'seam' | 'derived';
|
|
54
66
|
}
|
|
@@ -66,7 +78,25 @@ export declare class SkillMcpService extends Service {
|
|
|
66
78
|
static inject: string[];
|
|
67
79
|
private readonly officialSkillDirs;
|
|
68
80
|
constructor(ctx: Context, config?: SkillConfig);
|
|
69
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* Skill roots that live **inside loaded plugin packages**, i.e.
|
|
83
|
+
* `<node_modules>/<pkg>/skills`.
|
|
84
|
+
*
|
|
85
|
+
* The host-level skill filesystem is disabled in web-app (presets own
|
|
86
|
+
* discovery), so a plugin that ships skills — `@max-null/dsh-skills` and
|
|
87
|
+
* `@max-null/dsh-plugin-center` both do — keeps them on disk inside its own
|
|
88
|
+
* package, where no user-level root can see them. Without this the
|
|
89
|
+
* management surface showed 17 user skills while 8 plugin skills were loaded
|
|
90
|
+
* and in effect (2026-09-14 用户报「skill 生效但不展示」).
|
|
91
|
+
*
|
|
92
|
+
* Only **loaded** entries are probed, so this costs one existence check per
|
|
93
|
+
* plugin rather than a scan of the whole node_modules tree.
|
|
94
|
+
*/
|
|
95
|
+
private pluginSkillDirs;
|
|
96
|
+
/**
|
|
97
|
+
* User-level skills, project-level skills for the given workspace, skills
|
|
98
|
+
* bundled inside loaded plugin packages, and any configured official roots.
|
|
99
|
+
*/
|
|
70
100
|
listSkills(cwd?: string): Promise<SkillView[]>;
|
|
71
101
|
/** Flip one disk-backed skill's model invocation by rewriting its SKILL.md frontmatter. */
|
|
72
102
|
toggleSkill(path: string): Promise<SkillView>;
|
package/dist/service.js
CHANGED
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
* `ctx.mcpStatus` seam with a derived fallback.
|
|
8
8
|
*/
|
|
9
9
|
import { Service } from '@deepseek-ai/cordis';
|
|
10
|
+
import { existsSync } from 'node:fs';
|
|
10
11
|
import { readdir, readFile, writeFile } from 'node:fs/promises';
|
|
11
12
|
import { homedir } from 'node:os';
|
|
12
13
|
import { join, sep } from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
13
15
|
import { parseSkillFrontmatter, setDisableModelInvocation } from "./frontmatter.js";
|
|
14
16
|
/** Runtime mirror of cordis FiberState (a cross-package const enum). */
|
|
15
17
|
const FIBER_PHASE = {
|
|
@@ -48,6 +50,21 @@ const SKILL_ROOTS = [
|
|
|
48
50
|
{ path: join(homedir(), '.dsh', 'skills'), source: 'user-dsh' },
|
|
49
51
|
{ path: join(homedir(), '.agents', 'skills'), source: 'user-agents' },
|
|
50
52
|
];
|
|
53
|
+
/**
|
|
54
|
+
* This package's own `node_modules` directory — the tree every loaded plugin
|
|
55
|
+
* was resolved from.
|
|
56
|
+
*
|
|
57
|
+
* Derived from the module URL instead of assuming
|
|
58
|
+
* `$DSH_HOME/profiles/<p>/node_modules`: the profile layout and even the
|
|
59
|
+
* directory name vary, while this module is always inside that tree.
|
|
60
|
+
* @returns the node_modules path, or null when the module is not under one.
|
|
61
|
+
*/
|
|
62
|
+
function ownNodeModules() {
|
|
63
|
+
const here = fileURLToPath(import.meta.url);
|
|
64
|
+
const marker = `${sep}node_modules${sep}`;
|
|
65
|
+
const at = here.lastIndexOf(marker);
|
|
66
|
+
return at < 0 ? null : here.slice(0, at + marker.length - 1);
|
|
67
|
+
}
|
|
51
68
|
/** Absolute SKILL.md path for one directory/file entry, or null. */
|
|
52
69
|
function skillPathFor(root, name, isDirectory) {
|
|
53
70
|
if (isDirectory)
|
|
@@ -100,7 +117,39 @@ export class SkillMcpService extends Service {
|
|
|
100
117
|
super(ctx, 'skillMcp');
|
|
101
118
|
this.officialSkillDirs = config.officialSkillDirs ?? [];
|
|
102
119
|
}
|
|
103
|
-
/**
|
|
120
|
+
/**
|
|
121
|
+
* Skill roots that live **inside loaded plugin packages**, i.e.
|
|
122
|
+
* `<node_modules>/<pkg>/skills`.
|
|
123
|
+
*
|
|
124
|
+
* The host-level skill filesystem is disabled in web-app (presets own
|
|
125
|
+
* discovery), so a plugin that ships skills — `@max-null/dsh-skills` and
|
|
126
|
+
* `@max-null/dsh-plugin-center` both do — keeps them on disk inside its own
|
|
127
|
+
* package, where no user-level root can see them. Without this the
|
|
128
|
+
* management surface showed 17 user skills while 8 plugin skills were loaded
|
|
129
|
+
* and in effect (2026-09-14 用户报「skill 生效但不展示」).
|
|
130
|
+
*
|
|
131
|
+
* Only **loaded** entries are probed, so this costs one existence check per
|
|
132
|
+
* plugin rather than a scan of the whole node_modules tree.
|
|
133
|
+
*/
|
|
134
|
+
pluginSkillDirs() {
|
|
135
|
+
const nm = ownNodeModules();
|
|
136
|
+
if (nm === null)
|
|
137
|
+
return [];
|
|
138
|
+
const out = [];
|
|
139
|
+
for (const entry of this.ctx.loader.entries()) {
|
|
140
|
+
const name = entry.options.name;
|
|
141
|
+
if (typeof name !== 'string' || name === '' || name.startsWith('.') || name.startsWith('/'))
|
|
142
|
+
continue;
|
|
143
|
+
const dir = join(nm, ...name.split('/'), 'skills');
|
|
144
|
+
if (existsSync(dir))
|
|
145
|
+
out.push({ dir, label: `plugin:${name}` });
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* User-level skills, project-level skills for the given workspace, skills
|
|
151
|
+
* bundled inside loaded plugin packages, and any configured official roots.
|
|
152
|
+
*/
|
|
104
153
|
async listSkills(cwd) {
|
|
105
154
|
const skills = [];
|
|
106
155
|
for (const root of SKILL_ROOTS) {
|
|
@@ -110,6 +159,9 @@ export class SkillMcpService extends Service {
|
|
|
110
159
|
skills.push(...await scanSkillRoot(join(cwd, '.agents', 'skills'), 'project-agents'));
|
|
111
160
|
skills.push(...await scanSkillRoot(join(cwd, '.dsh', 'skills'), 'project-dsh'));
|
|
112
161
|
}
|
|
162
|
+
for (const plugin of this.pluginSkillDirs()) {
|
|
163
|
+
skills.push(...await scanSkillRoot(plugin.dir, plugin.label, false, 'plugin'));
|
|
164
|
+
}
|
|
113
165
|
for (const dir of this.officialSkillDirs) {
|
|
114
166
|
skills.push(...await scanSkillRoot(dir, 'bundled', false, 'dsh-official'));
|
|
115
167
|
}
|
|
@@ -150,6 +202,8 @@ export class SkillMcpService extends Service {
|
|
|
150
202
|
if (cwd !== undefined && cwd !== '') {
|
|
151
203
|
roots.push(join(cwd, '.agents', 'skills'), join(cwd, '.dsh', 'skills'));
|
|
152
204
|
}
|
|
205
|
+
for (const plugin of this.pluginSkillDirs())
|
|
206
|
+
roots.push(plugin.dir);
|
|
153
207
|
roots.push(...this.officialSkillDirs);
|
|
154
208
|
// Segment-boundary prefix check: a sibling directory like `skills-notes`
|
|
155
209
|
// must not satisfy a `skills` root. join(root, '') normalizes to root.
|
|
@@ -211,29 +265,40 @@ export class SkillMcpService extends Service {
|
|
|
211
265
|
/** Runtime status per server: upstream `mcpStatus` seam when present, else derived. */
|
|
212
266
|
async mcpStatus() {
|
|
213
267
|
const servers = await this.listMcpServers();
|
|
268
|
+
// Tool identity always comes from the mounted tool table — `schemas()`
|
|
269
|
+
// whitelists name/description, which is exactly what the surface needs.
|
|
270
|
+
// The seam, when present, only refines the count and connection facts.
|
|
271
|
+
const schemas = this.ctx.tools.schemas();
|
|
272
|
+
const toolsOf = (serverName) => {
|
|
273
|
+
const prefix = `mcp__${serverName}__`;
|
|
274
|
+
return schemas
|
|
275
|
+
.filter(s => s.name.startsWith(prefix))
|
|
276
|
+
.map(s => ({ name: s.name, description: s.description ?? '' }));
|
|
277
|
+
};
|
|
214
278
|
const seam = this.ctx.get('mcpStatus');
|
|
215
279
|
if (seam !== undefined) {
|
|
216
280
|
const byName = new Map(seam.list().map(s => [s.serverName, s]));
|
|
217
281
|
return servers.map(s => {
|
|
218
282
|
const st = byName.get(s.serverName);
|
|
283
|
+
const tools = toolsOf(s.serverName);
|
|
219
284
|
return {
|
|
220
285
|
serverName: s.serverName,
|
|
221
286
|
fiberPhase: s.fiberPhase,
|
|
222
|
-
toolCount: st?.toolCount ??
|
|
287
|
+
toolCount: st?.toolCount ?? tools.length,
|
|
288
|
+
tools,
|
|
223
289
|
connected: st?.phase === 'connected',
|
|
224
290
|
statusSource: 'seam',
|
|
225
291
|
};
|
|
226
292
|
});
|
|
227
293
|
}
|
|
228
|
-
const toolNames = this.ctx.tools.schemas().map(s => s.name);
|
|
229
294
|
return servers.map(s => {
|
|
230
|
-
const
|
|
231
|
-
const toolCount = toolNames.filter(n => n.startsWith(prefix)).length;
|
|
295
|
+
const tools = toolsOf(s.serverName);
|
|
232
296
|
return {
|
|
233
297
|
serverName: s.serverName,
|
|
234
298
|
fiberPhase: s.fiberPhase,
|
|
235
|
-
toolCount,
|
|
236
|
-
|
|
299
|
+
toolCount: tools.length,
|
|
300
|
+
tools,
|
|
301
|
+
connected: !s.disabled && s.fiberPhase === 'active' && tools.length > 0,
|
|
237
302
|
statusSource: 'derived',
|
|
238
303
|
};
|
|
239
304
|
});
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@max-null/dsh-skill-mcp-center",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Skill & MCP management center for DeepSeek Harness — manage skills and MCP servers in Settings, live MCP status in the sidebar",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -21,8 +21,7 @@
|
|
|
21
21
|
"files": [
|
|
22
22
|
"dist",
|
|
23
23
|
"client.js",
|
|
24
|
-
"cordis.patch.yml"
|
|
25
|
-
],
|
|
24
|
+
"cordis.patch.yml", "docs/shots"],
|
|
26
25
|
"dsh": {
|
|
27
26
|
"bundle": {
|
|
28
27
|
"patch": "./cordis.patch.yml"
|
|
@@ -56,7 +55,8 @@
|
|
|
56
55
|
},
|
|
57
56
|
"scripts": {
|
|
58
57
|
"build": "tsc -p tsconfig.json && node build-client.mjs",
|
|
59
|
-
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
58
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
59
|
+
"test": "vitest run"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
62
|
"js-yaml": "^4.1.0"
|
|
@@ -81,6 +81,7 @@
|
|
|
81
81
|
"@types/react": "~18.3.1",
|
|
82
82
|
"esbuild": "^0.24.0",
|
|
83
83
|
"react": "^18.2.0",
|
|
84
|
-
"typescript": "^5.5.0"
|
|
84
|
+
"typescript": "^5.5.0",
|
|
85
|
+
"vitest": "^4.1.11"
|
|
85
86
|
}
|
|
86
87
|
}
|