@max-null/dsh-skill-mcp-center 0.4.2 → 0.5.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.md +24 -3
- package/client.js +82 -18
- package/dist/rpc.d.ts +35 -3
- package/dist/rpc.js +109 -44
- 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 +4 -2
- package/release-notes.md +35 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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-
|
|
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
4
|
|
|
5
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
6
|
|
|
@@ -10,11 +10,32 @@ Skill 与 MCP 管理中心:在设置里管理 skills 与 MCP 服务器,右
|
|
|
10
10
|
|
|
11
11
|
## Features / 功能
|
|
12
12
|
|
|
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).
|
|
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.
|
|
14
14
|
- **MCP management / MCP 管理** — add / edit / remove `mcp-client` servers, enable/disable without deleting config, all **hot-applied** through `ctx.loader` (no restart).
|
|
15
|
-
- **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.
|
|
16
16
|
- **Skin-compatible / 皮肤兼容** — every color uses `var(--dsw-*)` tokens.
|
|
17
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
|
+
|
|
18
39
|
## Install / 安装
|
|
19
40
|
|
|
20
41
|
```sh
|
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
|
{
|
|
@@ -879,7 +924,18 @@ var inject = ["slots", "connection", "locale"];
|
|
|
879
924
|
function apply(ctx) {
|
|
880
925
|
injectCss();
|
|
881
926
|
rpc = async (endpoint, payload = {}) => {
|
|
882
|
-
const
|
|
927
|
+
const response = await fetch("/api/skill-mcp", {
|
|
928
|
+
method: "POST",
|
|
929
|
+
headers: { "content-type": "application/json" },
|
|
930
|
+
body: JSON.stringify({ endpoint, payload })
|
|
931
|
+
});
|
|
932
|
+
if (response.status === 404) {
|
|
933
|
+
const legacy = await ctx.connection.rpc.call("/skill-mcp", endpoint, payload);
|
|
934
|
+
if (legacy.ok) return legacy.value;
|
|
935
|
+
throw new Error(legacy.error?.message ?? `skill-mcp-center: ${endpoint} failed`);
|
|
936
|
+
}
|
|
937
|
+
if (!response.ok) throw new Error(`skill-mcp-center: ${endpoint} failed (HTTP ${response.status})`);
|
|
938
|
+
const result = await response.json();
|
|
883
939
|
if (result.ok) return result.value;
|
|
884
940
|
throw new Error(result.error?.message ?? `skill-mcp-center: ${endpoint} failed`);
|
|
885
941
|
};
|
|
@@ -914,7 +970,14 @@ function apply(ctx) {
|
|
|
914
970
|
sidebarCtx.effect(() => service.registerTab({
|
|
915
971
|
id: "@max-null/dsh-skill-mcp-center:mcp",
|
|
916
972
|
title: () => "MCP",
|
|
917
|
-
|
|
973
|
+
// 图标风格对齐 DSH 原生右栏:彩色线性、22px(「开始」页的图标容器
|
|
974
|
+
// .geFEbW_entryIcon 是 26px、自带占位图标 22px,整列基准即 22 ——
|
|
975
|
+
// 2026-09-14 用户指「尺寸 14 不太对,其他都在 22 左右」)。
|
|
976
|
+
// **必须是返回 ReactNode 的函数**:DSH 原生右侧栏只认函数形式,直接传 JSX 元素
|
|
977
|
+
// 会被忽略并回落到它的占位图标(`geFEbW_placeholder`,22px 灰圆)——
|
|
978
|
+
// 2026-09-14 实测:同一份 icon 在 better-sidebar 自绘的底部工作台里正常显示,
|
|
979
|
+
// 在原生右栏「开始」页却是占位符,据此定位到形式差异。
|
|
980
|
+
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
981
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: "2", y: "2", width: "20", height: "8", rx: "2", ry: "2" }),
|
|
919
982
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: "2", y: "14", width: "20", height: "8", rx: "2", ry: "2" }),
|
|
920
983
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "6", y1: "6", x2: "6.01", y2: "6" }),
|
|
@@ -927,7 +990,8 @@ function apply(ctx) {
|
|
|
927
990
|
sidebarCtx.effect(() => service.registerTab({
|
|
928
991
|
id: "@max-null/dsh-skill-mcp-center:skills",
|
|
929
992
|
title: () => "Skill",
|
|
930
|
-
|
|
993
|
+
// 同上:必须是返回 ReactNode 的函数(DSH 原生右栏只认函数形式,传元素会回落占位图标)
|
|
994
|
+
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
995
|
order: 71,
|
|
932
996
|
single: true,
|
|
933
997
|
component: (props) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SidebarSkillTab, { visible: props.visible, cwd: props.scope?.cwd })
|
package/dist/rpc.d.ts
CHANGED
|
@@ -1,11 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `SkillMcpRpc` —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* `SkillMcpRpc` — the host half of the browser's private RPC entry point.
|
|
3
|
+
*
|
|
4
|
+
* Transport (2026-09-17). Two host shapes are supported because the two
|
|
5
|
+
* compositions this plugin runs under differ:
|
|
6
|
+
*
|
|
7
|
+
* - **Newer hosts (0.1.5 line, e.g. the web source build)** expose
|
|
8
|
+
* `connection.fetch.register(...)`. This route is used there because
|
|
9
|
+
* `connection.rpc.handle('/skill-mcp', …)` ends at `owner.webServer.register()`
|
|
10
|
+
* inside Connection (`client-connection/src/rpc-host.ts:179`) and the owning
|
|
11
|
+
* fiber declares no `webServer` injection, so it throws
|
|
12
|
+
* `cannot get property "webServer" without inject`; every channel registered
|
|
13
|
+
* that way fails and the browser sees `HTTP 405` from the static fallback.
|
|
14
|
+
* `fetch.register` only writes Connection's own route map, so one exact route
|
|
15
|
+
* under the shared `/api` channel serves the same endpoints.
|
|
16
|
+
*
|
|
17
|
+
* - **Older hosts (e.g. the packaged SSiD kernel)** expose only the logical
|
|
18
|
+
* channel registry, where `rpc.handle` works, so it stays as the fallback.
|
|
19
|
+
*
|
|
20
|
+
* `connection` is resolved with `ctx.inject` rather than a declared dependency:
|
|
21
|
+
* it activates after this plugin, and `ctx.plugin()` does not wait for
|
|
22
|
+
* dependencies.
|
|
5
23
|
*/
|
|
6
24
|
import { Service, type Context } from '@deepseek-ai/cordis';
|
|
25
|
+
/** Exact Fetch route serving this plugin's endpoints under the shared channel. */
|
|
26
|
+
export declare const ROUTE = "/api/skill-mcp";
|
|
27
|
+
/** Logical channel used on hosts without exact Fetch-route registration. */
|
|
28
|
+
export declare const LEGACY_CHANNEL = "/skill-mcp";
|
|
29
|
+
/** Body the browser half posts to {@link ROUTE}. */
|
|
30
|
+
export interface SkillMcpRequest {
|
|
31
|
+
/** Endpoint name, e.g. `listSkills`. */
|
|
32
|
+
readonly endpoint: string;
|
|
33
|
+
/** Endpoint arguments. */
|
|
34
|
+
readonly payload?: unknown;
|
|
35
|
+
}
|
|
7
36
|
export declare class SkillMcpRpc extends Service {
|
|
8
37
|
static inject: string[];
|
|
38
|
+
/**
|
|
39
|
+
* @param ctx - plugin context carrying the skill/MCP engine.
|
|
40
|
+
*/
|
|
9
41
|
constructor(ctx: Context);
|
|
10
42
|
}
|
|
11
43
|
export default SkillMcpRpc;
|
package/dist/rpc.js
CHANGED
|
@@ -1,58 +1,123 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `SkillMcpRpc` —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* `SkillMcpRpc` — the host half of the browser's private RPC entry point.
|
|
3
|
+
*
|
|
4
|
+
* Transport (2026-09-17). Two host shapes are supported because the two
|
|
5
|
+
* compositions this plugin runs under differ:
|
|
6
|
+
*
|
|
7
|
+
* - **Newer hosts (0.1.5 line, e.g. the web source build)** expose
|
|
8
|
+
* `connection.fetch.register(...)`. This route is used there because
|
|
9
|
+
* `connection.rpc.handle('/skill-mcp', …)` ends at `owner.webServer.register()`
|
|
10
|
+
* inside Connection (`client-connection/src/rpc-host.ts:179`) and the owning
|
|
11
|
+
* fiber declares no `webServer` injection, so it throws
|
|
12
|
+
* `cannot get property "webServer" without inject`; every channel registered
|
|
13
|
+
* that way fails and the browser sees `HTTP 405` from the static fallback.
|
|
14
|
+
* `fetch.register` only writes Connection's own route map, so one exact route
|
|
15
|
+
* under the shared `/api` channel serves the same endpoints.
|
|
16
|
+
*
|
|
17
|
+
* - **Older hosts (e.g. the packaged SSiD kernel)** expose only the logical
|
|
18
|
+
* channel registry, where `rpc.handle` works, so it stays as the fallback.
|
|
19
|
+
*
|
|
20
|
+
* `connection` is resolved with `ctx.inject` rather than a declared dependency:
|
|
21
|
+
* it activates after this plugin, and `ctx.plugin()` does not wait for
|
|
22
|
+
* dependencies.
|
|
5
23
|
*/
|
|
6
24
|
import { Service } from '@deepseek-ai/cordis';
|
|
7
|
-
|
|
25
|
+
/** Exact Fetch route serving this plugin's endpoints under the shared channel. */
|
|
26
|
+
export const ROUTE = '/api/skill-mcp';
|
|
27
|
+
/** Logical channel used on hosts without exact Fetch-route registration. */
|
|
28
|
+
export const LEGACY_CHANNEL = '/skill-mcp';
|
|
8
29
|
function internal(message) {
|
|
9
30
|
return { ok: false, error: { code: 'internal', message, details: {} } };
|
|
10
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Wrap one endpoint result as a JSON response.
|
|
34
|
+
* @param value - RPC result envelope.
|
|
35
|
+
* @returns response carrying the envelope.
|
|
36
|
+
*/
|
|
37
|
+
function jsonResponse(value) {
|
|
38
|
+
return new Response(JSON.stringify(value), { headers: { 'content-type': 'application/json' } });
|
|
39
|
+
}
|
|
11
40
|
export class SkillMcpRpc extends Service {
|
|
12
|
-
static inject = ['skillMcp'
|
|
41
|
+
static inject = ['skillMcp'];
|
|
42
|
+
/**
|
|
43
|
+
* @param ctx - plugin context carrying the skill/MCP engine.
|
|
44
|
+
*/
|
|
13
45
|
constructor(ctx) {
|
|
14
46
|
super(ctx, 'skillMcpRpc');
|
|
15
|
-
ctx.connection
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
ctx.inject(['connection'], (connectionCtx) => {
|
|
48
|
+
// Both reads are attempted: the published peer types predate the
|
|
49
|
+
// `connection` declaration on Context, so neither is typed here.
|
|
50
|
+
const surface = connectionCtx;
|
|
51
|
+
const connection = surface.connection ?? surface.get?.('connection');
|
|
52
|
+
if (connection === undefined)
|
|
53
|
+
return;
|
|
54
|
+
if (typeof connection.fetch?.register === 'function') {
|
|
55
|
+
connection.fetch.register({
|
|
56
|
+
path: ROUTE,
|
|
57
|
+
methods: ['POST'],
|
|
58
|
+
requestBody: 'buffered',
|
|
59
|
+
fetch: async (request) => {
|
|
60
|
+
let body;
|
|
61
|
+
try {
|
|
62
|
+
body = await request.json();
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return jsonResponse(internal('request body must be JSON'));
|
|
66
|
+
}
|
|
67
|
+
return jsonResponse(await dispatch(ctx, body));
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
connection.rpc.handle(LEGACY_CHANNEL, (endpoint, payload) => dispatch(ctx, { endpoint, payload }));
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Run one endpoint against the engine.
|
|
78
|
+
* @param ctx - plugin context carrying the skill/MCP engine.
|
|
79
|
+
* @param body - decoded request body.
|
|
80
|
+
* @returns the endpoint result, or an `internal` failure.
|
|
81
|
+
*/
|
|
82
|
+
async function dispatch(ctx, body) {
|
|
83
|
+
try {
|
|
84
|
+
const p = (body.payload ?? {});
|
|
85
|
+
switch (body.endpoint) {
|
|
86
|
+
case 'listSkills': {
|
|
87
|
+
const cwd = p.cwd;
|
|
88
|
+
return { ok: true, value: await ctx.skillMcp.listSkills(typeof cwd === 'string' ? cwd : undefined) };
|
|
51
89
|
}
|
|
52
|
-
|
|
53
|
-
|
|
90
|
+
case 'toggleSkill': {
|
|
91
|
+
const path = p.path;
|
|
92
|
+
if (typeof path !== 'string' || path === '')
|
|
93
|
+
return internal('toggleSkill: path is required');
|
|
94
|
+
return { ok: true, value: await ctx.skillMcp.toggleSkill(path) };
|
|
54
95
|
}
|
|
55
|
-
|
|
96
|
+
case 'readSkill': {
|
|
97
|
+
const path = p.path;
|
|
98
|
+
if (typeof path !== 'string' || path === '')
|
|
99
|
+
return internal('readSkill: path is required');
|
|
100
|
+
const cwd = p.cwd;
|
|
101
|
+
return { ok: true, value: await ctx.skillMcp.readSkill(path, typeof cwd === 'string' ? cwd : undefined) };
|
|
102
|
+
}
|
|
103
|
+
case 'listMcpServers':
|
|
104
|
+
return { ok: true, value: await ctx.skillMcp.listMcpServers() };
|
|
105
|
+
case 'createMcpServer':
|
|
106
|
+
return { ok: true, value: await ctx.skillMcp.createMcpServer(p.config) };
|
|
107
|
+
case 'updateMcpServer':
|
|
108
|
+
return { ok: true, value: await ctx.skillMcp.updateMcpServer(String(p.id), p.config) };
|
|
109
|
+
case 'removeMcpServer':
|
|
110
|
+
return { ok: true, value: await ctx.skillMcp.removeMcpServer(String(p.id)) };
|
|
111
|
+
case 'setMcpServerEnabled':
|
|
112
|
+
return { ok: true, value: await ctx.skillMcp.setMcpServerEnabled(String(p.id), p.enabled === true) };
|
|
113
|
+
case 'mcpStatus':
|
|
114
|
+
return { ok: true, value: await ctx.skillMcp.mcpStatus() };
|
|
115
|
+
default:
|
|
116
|
+
return internal(`unknown endpoint "${body.endpoint}"`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
return internal(error instanceof Error ? error.message : String(error));
|
|
56
121
|
}
|
|
57
122
|
}
|
|
58
123
|
export default SkillMcpRpc;
|
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.1",
|
|
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,7 +21,9 @@
|
|
|
21
21
|
"files": [
|
|
22
22
|
"dist",
|
|
23
23
|
"client.js",
|
|
24
|
-
"cordis.patch.yml"
|
|
24
|
+
"cordis.patch.yml",
|
|
25
|
+
"docs/shots",
|
|
26
|
+
"release-notes.md"
|
|
25
27
|
],
|
|
26
28
|
"dsh": {
|
|
27
29
|
"bundle": {
|
package/release-notes.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Release Notes — @max-null/dsh-skill-mcp-center
|
|
2
|
+
|
|
3
|
+
## 0.5.1 (2026-09-17)
|
|
4
|
+
|
|
5
|
+
### 修复
|
|
6
|
+
|
|
7
|
+
- 修正 web 环境下设置面板的「加载失败:transport failure for /skill-mcp/listSkills: HTTP 405」。
|
|
8
|
+
根因在宿主侧:`ctx.connection.rpc.handle()` 注册逻辑通道时会访问 `client-connection`
|
|
9
|
+
未声明的 `webServer` 注入,cordis 属性代理抛错后注册静默失败(DSH 源码形态特有;
|
|
10
|
+
打包内核形态不受影响)。上游报告:deepseek-ai/deepseek-harness discussions #6880。
|
|
11
|
+
|
|
12
|
+
### 变更
|
|
13
|
+
|
|
14
|
+
- 传输层改为双路径自适应:
|
|
15
|
+
- 宿主提供 `connection.fetch.register` → 注册 `POST /api/skill-mcp` 精确路由
|
|
16
|
+
(只写 Connection 自己的路由表,不触碰 `owner.webServer`);
|
|
17
|
+
- 宿主只提供逻辑通道注册表(老打包内核)→ 自动回退 `/skill-mcp`;
|
|
18
|
+
- 浏览器侧先试新路由,**收到 404 才回退**(新路由未注册时 `/api/*` 是 404,根路径才是 405)。
|
|
19
|
+
- `connection` 服务改用 `ctx.inject(['connection'], cb)` 获取:它在插件 apply 时尚未就绪,
|
|
20
|
+
且 `ctx.plugin(P)` 不等待依赖。
|
|
21
|
+
|
|
22
|
+
### 兼容性
|
|
23
|
+
|
|
24
|
+
- 无 API 变更:端点集合、请求体、响应信封(RpcResult)与 peer 依赖均未变。
|
|
25
|
+
- 面板端与宿主端需同版本升级(浏览器侧的传输策略随之切换)。
|
|
26
|
+
|
|
27
|
+
### 测试
|
|
28
|
+
|
|
29
|
+
- 新增 `tests/rpc-transport.test.ts`(8 条):传输层选择、服务晚到时不抢先注册、
|
|
30
|
+
路由派发、JSON 解析失败与引擎异常收敛为 `internal` 信封。
|
|
31
|
+
- 全量 18 条通过;`npm run typecheck` 无错。
|
|
32
|
+
|
|
33
|
+
### 验证
|
|
34
|
+
|
|
35
|
+
- web(源码形态)实测:`POST /api/skill-mcp` → 200,面板恢复,控制台 0 errors。
|