@max-null/dsh-skill-mcp-center 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +33 -0
- package/client.js +624 -0
- package/cordis.patch.yml +6 -0
- package/dist/frontmatter.d.ts +10 -0
- package/dist/frontmatter.js +39 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +10 -0
- package/dist/rpc.d.ts +11 -0
- package/dist/rpc.js +51 -0
- package/dist/service.d.ts +82 -0
- package/dist/service.js +213 -0
- package/package.json +86 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Max-Null
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# dsh-skill-mcp-center
|
|
2
|
+
|
|
3
|
+
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
|
+
|
|
5
|
+
Skill 与 MCP 管理中心:在设置里管理 skills 与 MCP 服务器,右侧边栏查看 MCP 实时状态。
|
|
6
|
+
|
|
7
|
+
## Features / 功能
|
|
8
|
+
|
|
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).
|
|
10
|
+
- **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 + tool count, polled while visible and following the session.
|
|
12
|
+
- **Skin-compatible / 皮肤兼容** — every color uses `var(--dsw-*)` tokens.
|
|
13
|
+
|
|
14
|
+
## Install / 安装
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
dsh plugin --profile web add github:Max-Null/dsh-skill-mcp-center
|
|
18
|
+
# or from npm, once published / 或通过 npm(发布后)
|
|
19
|
+
dsh plugin --profile web add @max-null/dsh-skill-mcp-center
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Restart `dsh web`, then open Settings → Skill & MCP. The sidebar "MCP" tab appears only when `dsh-better-sidebar` is installed (optional peer).
|
|
23
|
+
|
|
24
|
+
## Development / 开发
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
pnpm install
|
|
28
|
+
pnpm build # tsc (host) + esbuild (browser bundle)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## License / 许可
|
|
32
|
+
|
|
33
|
+
[MIT](./LICENSE)
|
package/client.js
ADDED
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@max-null/dsh-skill-mcp-center",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
|
|
7
|
+
"use strict";
|
|
8
|
+
var __defProp = Object.defineProperty;
|
|
9
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
10
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
11
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
16
|
+
var __copyProps = (to, from, except, desc) => {
|
|
17
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
18
|
+
for (let key of __getOwnPropNames(from))
|
|
19
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
20
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
21
|
+
}
|
|
22
|
+
return to;
|
|
23
|
+
};
|
|
24
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
25
|
+
|
|
26
|
+
// client/index.tsx
|
|
27
|
+
var index_exports = {};
|
|
28
|
+
__export(index_exports, {
|
|
29
|
+
apply: () => apply,
|
|
30
|
+
inject: () => inject
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(index_exports);
|
|
33
|
+
var import_react = require("react");
|
|
34
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
35
|
+
var CSS = `
|
|
36
|
+
.smc-title { font-size: 18px; font-weight: 600; line-height: 26px; color: var(--dsw-alias-label-primary); }
|
|
37
|
+
.smc-sub { font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-tertiary); }
|
|
38
|
+
.smc-tabs { display: flex; gap: 4px; margin: 16px 0 12px; border-bottom: 1px solid var(--dsw-alias-border-l2); }
|
|
39
|
+
.smc-tab { padding: 8px 14px; font-size: 14px; cursor: pointer; background: none; border: none; font-family: inherit; color: var(--dsw-alias-label-tertiary); border-bottom: 2px solid transparent; margin-bottom: -1px; }
|
|
40
|
+
.smc-tab:hover { color: var(--dsw-alias-label-secondary); }
|
|
41
|
+
.smc-tab.active { color: var(--dsw-alias-label-primary); border-bottom-color: var(--dsw-alias-state-business-primary); }
|
|
42
|
+
|
|
43
|
+
.smc-card { border: 1px solid var(--dsw-alias-border-l2); border-radius: 10px; padding: 14px 16px; margin-bottom: 10px; background: var(--dsw-alias-bg-layer-1); min-width: 0; }
|
|
44
|
+
.smc-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
45
|
+
.smc-name { font-weight: 600; color: var(--dsw-alias-label-primary); }
|
|
46
|
+
.smc-desc { color: var(--dsw-alias-label-secondary); font-size: 13px; margin-top: 4px; }
|
|
47
|
+
.smc-meta { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-top: 10px; }
|
|
48
|
+
.smc-spacer { flex: 1; }
|
|
49
|
+
|
|
50
|
+
.smc-badge { display: inline-flex; align-items: center; height: 20px; padding: 0 8px; border-radius: 6px; font-size: 12px; line-height: 1; background: var(--dsw-alias-bg-module-platform); color: var(--dsw-alias-label-secondary); }
|
|
51
|
+
.smc-badge.user { background: var(--dsw-alias-state-business-tertiary); color: var(--dsw-alias-state-business-primary); }
|
|
52
|
+
.smc-badge.workspace { background: var(--dsw-alias-state-success-tertiary); color: var(--dsw-alias-state-success-primary); }
|
|
53
|
+
.smc-badge.bundled { background: var(--dsw-alias-bg-module-platform); color: var(--dsw-alias-label-tertiary); }
|
|
54
|
+
.smc-badge.runtime { background: var(--dsw-alias-state-warn-tertiary); color: var(--dsw-alias-state-warn-primary); }
|
|
55
|
+
.smc-badge.disabled { background: var(--dsw-alias-bg-module-platform); color: var(--dsw-alias-label-caption); }
|
|
56
|
+
|
|
57
|
+
.smc-btn { height: 26px; padding: 0 10px; border-radius: 8px; border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-module-platform); color: var(--dsw-alias-label-primary); font-size: 12px; cursor: pointer; font-family: inherit; }
|
|
58
|
+
.smc-btn:hover { background: var(--dsw-alias-interactive-bg-hover); }
|
|
59
|
+
.smc-btn.primary { border: none; background: var(--dsw-alias-button-primary-fill); color: var(--dsw-alias-bg-base); }
|
|
60
|
+
.smc-btn.primary:hover { background: var(--dsw-alias-button-primary-hover); }
|
|
61
|
+
.smc-btn.danger { border-color: var(--dsw-alias-state-error-primary); color: var(--dsw-alias-state-error-primary); background: none; }
|
|
62
|
+
.smc-btn.danger:hover { background: var(--dsw-alias-interactive-bg-hover); }
|
|
63
|
+
.smc-btn:disabled { opacity: 0.45; cursor: default; }
|
|
64
|
+
|
|
65
|
+
.smc-dot { flex: none; width: 8px; height: 8px; border-radius: 50%; background: var(--dsw-alias-state-success-primary); }
|
|
66
|
+
.smc-dot.idle { background: var(--dsw-alias-label-caption); }
|
|
67
|
+
.smc-dot.failed { background: var(--dsw-alias-state-error-primary); }
|
|
68
|
+
|
|
69
|
+
.smc-toggle { position: relative; width: 34px; height: 20px; border-radius: 10px; border: none; cursor: pointer; background: var(--dsw-alias-bg-module-platform); transition: background .15s; }
|
|
70
|
+
.smc-toggle.on { background: var(--dsw-alias-state-success-primary); }
|
|
71
|
+
.smc-toggle::after { content: ''; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; border-radius: 50%; background: var(--dsw-alias-bg-base); transition: left .15s; }
|
|
72
|
+
.smc-toggle.on::after { left: 16px; }
|
|
73
|
+
.smc-toggle:disabled { opacity: 0.4; cursor: default; }
|
|
74
|
+
|
|
75
|
+
.smc-input { height: 28px; padding: 0 10px; border-radius: 8px; 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; font-family: inherit; }
|
|
76
|
+
.smc-input:focus { border-color: var(--dsw-alias-state-business-primary); }
|
|
77
|
+
.smc-select { height: 28px; padding: 0 8px; border-radius: 8px; border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-secondary); font-size: 13px; cursor: pointer; font-family: inherit; }
|
|
78
|
+
.smc-form { border: 1px solid var(--dsw-alias-border-l2); border-radius: 10px; padding: 16px; margin-bottom: 14px; background: var(--dsw-alias-bg-layer-2); }
|
|
79
|
+
.smc-field { display: flex; flex-direction: column; gap: 4px; margin-bottom: 12px; }
|
|
80
|
+
.smc-label { font-size: 12px; color: var(--dsw-alias-label-tertiary); }
|
|
81
|
+
.smc-error { font-size: 12px; color: var(--dsw-alias-state-error-primary); }
|
|
82
|
+
|
|
83
|
+
.smc-toast { position: fixed; bottom: 32px; left: 50%; transform: translateX(-50%); padding: 10px 18px; border-radius: 10px; background: var(--dsw-alias-bg-layer-1); border: 1px solid var(--dsw-alias-border-l2); color: var(--dsw-alias-label-primary); font-size: 13px; box-shadow: 0 8px 32px rgba(0,0,0,.18); z-index: 200; max-width: 80vw; }
|
|
84
|
+
.smc-toast.ok { border-color: var(--dsw-alias-state-success-primary); }
|
|
85
|
+
.smc-toast.error { border-color: var(--dsw-alias-state-error-primary); }
|
|
86
|
+
|
|
87
|
+
.smc-sidebar { display: flex; flex-direction: column; gap: 2px; padding: 8px; }
|
|
88
|
+
.smc-srv { display: flex; align-items: center; gap: 8px; padding: 8px 10px; border-radius: 8px; }
|
|
89
|
+
.smc-srv:hover { background: var(--dsw-alias-interactive-bg-hover); }
|
|
90
|
+
.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); }
|
|
91
|
+
.smc-srv-count { font-size: 11px; color: var(--dsw-alias-label-tertiary); }
|
|
92
|
+
.smc-srv-state { font-size: 11px; color: var(--dsw-alias-label-caption); padding: 0 10px 6px 26px; }
|
|
93
|
+
.smc-empty { padding: 24px; text-align: center; color: var(--dsw-alias-label-tertiary); font-size: 12.5px; }
|
|
94
|
+
`;
|
|
95
|
+
var cssInjected = false;
|
|
96
|
+
function injectCss() {
|
|
97
|
+
if (cssInjected || typeof document === "undefined") return;
|
|
98
|
+
cssInjected = true;
|
|
99
|
+
const style = document.createElement("style");
|
|
100
|
+
style.setAttribute("data-plugin", "@max-null/dsh-skill-mcp-center");
|
|
101
|
+
style.textContent = CSS;
|
|
102
|
+
document.head.append(style);
|
|
103
|
+
}
|
|
104
|
+
var LOCALE_NS = "@max-null/dsh-skill-mcp-center";
|
|
105
|
+
var t = (key) => key;
|
|
106
|
+
var localeRevision = 0;
|
|
107
|
+
var localeListeners = /* @__PURE__ */ new Set();
|
|
108
|
+
function useLocale() {
|
|
109
|
+
const [rev, setRev] = (0, import_react.useState)(localeRevision);
|
|
110
|
+
(0, import_react.useEffect)(() => {
|
|
111
|
+
const l = () => {
|
|
112
|
+
setRev(localeRevision);
|
|
113
|
+
};
|
|
114
|
+
localeListeners.add(l);
|
|
115
|
+
return () => {
|
|
116
|
+
localeListeners.delete(l);
|
|
117
|
+
};
|
|
118
|
+
}, []);
|
|
119
|
+
return rev;
|
|
120
|
+
}
|
|
121
|
+
var zhDict = {
|
|
122
|
+
loading: "\u52A0\u8F7D\u4E2D\u2026",
|
|
123
|
+
loadFailed: "\u52A0\u8F7D\u5931\u8D25\uFF1A{e}",
|
|
124
|
+
noSkills: "\u672A\u53D1\u73B0\u4EFB\u4F55 skill\uFF08\u68C0\u67E5 ~/.dsh/skills \u4E0E\u9879\u76EE .agents/skills\uFF09",
|
|
125
|
+
sourceSystem: "\u7CFB\u7EDF",
|
|
126
|
+
sourceUser: "\u7528\u6237",
|
|
127
|
+
sourceWorkspace: "\u5DE5\u4F5C\u533A",
|
|
128
|
+
sourceRuntime: "\u8FD0\u884C\u65F6",
|
|
129
|
+
sectionSub: "\u7BA1\u7406\uFF08\u9759\u6001\u914D\u7F6E\uFF09\xB7 \u8FD0\u884C\u65F6\u72B6\u6001\u5728\u53F3\u4FA7\u8FB9\u680F",
|
|
130
|
+
modelDisabled: "\u6A21\u578B\u5DF2\u505C\u7528",
|
|
131
|
+
toggleModelVisible: "\u5207\u6362\u6A21\u578B\u53EF\u89C1",
|
|
132
|
+
skillReadonly: "system/runtime \u6280\u80FD\u53EA\u8BFB",
|
|
133
|
+
enable: "\u542F\u7528",
|
|
134
|
+
disable: "\u505C\u7528",
|
|
135
|
+
provider: "provider \xB7 {name}",
|
|
136
|
+
addServer: "\uFF0B \u6DFB\u52A0 server",
|
|
137
|
+
hotApplied: "\u589E\u5220\u6539\u70ED\u751F\u6548\uFF0C\u514D\u91CD\u542F",
|
|
138
|
+
edit: "\u7F16\u8F91",
|
|
139
|
+
remove: "\u5220\u9664",
|
|
140
|
+
disableServer: "\u505C\u7528\uFF08\u65AD\u5F00\u5E76\u91CA\u653E context\uFF09",
|
|
141
|
+
enableServer: "\u542F\u7528\uFF08\u91CD\u8FDE\uFF09",
|
|
142
|
+
serverNameLabel: "serverName\uFF08[A-Za-z0-9_-]{1,32}\uFF09",
|
|
143
|
+
transport: "transport",
|
|
144
|
+
command: "command",
|
|
145
|
+
argsLabel: "args\uFF08\u7A7A\u683C\u5206\u9694\uFF09",
|
|
146
|
+
cwdLabel: "cwd\uFF08\u53EF\u9009\uFF09",
|
|
147
|
+
url: "url",
|
|
148
|
+
cancel: "\u53D6\u6D88",
|
|
149
|
+
add: "\u6DFB\u52A0",
|
|
150
|
+
save: "\u4FDD\u5B58",
|
|
151
|
+
serverNameInvalid: "serverName \u9700\u5339\u914D [A-Za-z0-9_-]{1,32}",
|
|
152
|
+
added: "\u5DF2\u6DFB\u52A0 {name}\uFF0C\u70ED\u8FDE\u63A5",
|
|
153
|
+
updated: "\u5DF2\u66F4\u65B0 {name}\uFF0C\u70ED\u751F\u6548",
|
|
154
|
+
removed: "\u5DF2\u5220\u9664 {name}\uFF0C\u5DE5\u5177\u5DF2\u4E0B\u7EBF",
|
|
155
|
+
enabledToast: "\u5DF2\u542F\u7528 {name}\uFF08\u70ED\u751F\u6548\uFF09",
|
|
156
|
+
disabledToast: "\u5DF2\u505C\u7528 {name}\uFF08\u70ED\u751F\u6548\uFF09",
|
|
157
|
+
skillToggled: "\u5DF2\u5207\u6362 {name}\uFF0C\u6A21\u578B catalog \u5373\u65F6\u751F\u6548",
|
|
158
|
+
noMcpServer: "\u672A\u914D\u7F6E MCP server",
|
|
159
|
+
connected: "\u5DF2\u8FDE\u63A5",
|
|
160
|
+
notSynced: "\u672A\u540C\u6B65",
|
|
161
|
+
failed: "failed"
|
|
162
|
+
};
|
|
163
|
+
var enDict = {
|
|
164
|
+
loading: "Loading\u2026",
|
|
165
|
+
loadFailed: "Failed to load: {e}",
|
|
166
|
+
noSkills: "No skills found (check ~/.dsh/skills and project .agents/skills)",
|
|
167
|
+
sourceSystem: "system",
|
|
168
|
+
sourceUser: "user",
|
|
169
|
+
sourceWorkspace: "workspace",
|
|
170
|
+
sourceRuntime: "runtime",
|
|
171
|
+
sectionSub: "Manage (static config) \xB7 runtime status lives in the sidebar",
|
|
172
|
+
modelDisabled: "Model disabled",
|
|
173
|
+
toggleModelVisible: "Toggle model visibility",
|
|
174
|
+
skillReadonly: "system/runtime skills are read-only",
|
|
175
|
+
enable: "Enable",
|
|
176
|
+
disable: "Disable",
|
|
177
|
+
provider: "provider \xB7 {name}",
|
|
178
|
+
addServer: "\uFF0B Add server",
|
|
179
|
+
hotApplied: "hot-applied, no restart",
|
|
180
|
+
edit: "Edit",
|
|
181
|
+
remove: "Remove",
|
|
182
|
+
disableServer: "Disable (disconnect & free context)",
|
|
183
|
+
enableServer: "Enable (reconnect)",
|
|
184
|
+
serverNameLabel: "serverName ([A-Za-z0-9_-]{1,32})",
|
|
185
|
+
transport: "transport",
|
|
186
|
+
command: "command",
|
|
187
|
+
argsLabel: "args (space-separated)",
|
|
188
|
+
cwdLabel: "cwd (optional)",
|
|
189
|
+
url: "url",
|
|
190
|
+
cancel: "Cancel",
|
|
191
|
+
add: "Add",
|
|
192
|
+
save: "Save",
|
|
193
|
+
serverNameInvalid: "serverName must match [A-Za-z0-9_-]{1,32}",
|
|
194
|
+
added: "Added {name}, hot-connected",
|
|
195
|
+
updated: "Updated {name}, hot-applied",
|
|
196
|
+
removed: "Removed {name}, tools offline",
|
|
197
|
+
enabledToast: "{name} enabled (hot-applied)",
|
|
198
|
+
disabledToast: "{name} disabled (hot-applied)",
|
|
199
|
+
skillToggled: "Toggled {name}, model catalog updates live",
|
|
200
|
+
noMcpServer: "No MCP server",
|
|
201
|
+
connected: "Connected",
|
|
202
|
+
notSynced: "Not synced",
|
|
203
|
+
failed: "failed"
|
|
204
|
+
};
|
|
205
|
+
var rpc = async () => {
|
|
206
|
+
throw new Error("skill-mcp-center: rpc not wired");
|
|
207
|
+
};
|
|
208
|
+
var toastState = null;
|
|
209
|
+
var toastListeners = /* @__PURE__ */ new Set();
|
|
210
|
+
function showToast(message, kind = "ok") {
|
|
211
|
+
toastState = { message, kind };
|
|
212
|
+
toastListeners.forEach((l) => l());
|
|
213
|
+
}
|
|
214
|
+
function useToast() {
|
|
215
|
+
const [t2, setT] = (0, import_react.useState)(toastState);
|
|
216
|
+
(0, import_react.useEffect)(() => {
|
|
217
|
+
const l = () => {
|
|
218
|
+
setT(toastState);
|
|
219
|
+
};
|
|
220
|
+
toastListeners.add(l);
|
|
221
|
+
return () => {
|
|
222
|
+
toastListeners.delete(l);
|
|
223
|
+
};
|
|
224
|
+
}, []);
|
|
225
|
+
(0, import_react.useEffect)(() => {
|
|
226
|
+
if (t2 === null) return;
|
|
227
|
+
const id = setTimeout(() => {
|
|
228
|
+
toastState = null;
|
|
229
|
+
toastListeners.forEach((l) => l());
|
|
230
|
+
}, 3e3);
|
|
231
|
+
return () => clearTimeout(id);
|
|
232
|
+
}, [t2]);
|
|
233
|
+
return t2;
|
|
234
|
+
}
|
|
235
|
+
var SOURCE_GROUP = {
|
|
236
|
+
bundled: { label: "sourceSystem", cls: "bundled" },
|
|
237
|
+
"user-dsh": { label: "sourceUser", cls: "user" },
|
|
238
|
+
"user-agents": { label: "sourceUser", cls: "user" },
|
|
239
|
+
"project-dsh": { label: "sourceWorkspace", cls: "workspace" },
|
|
240
|
+
"project-agents": { label: "sourceWorkspace", cls: "workspace" },
|
|
241
|
+
runtime: { label: "sourceRuntime", cls: "runtime" }
|
|
242
|
+
};
|
|
243
|
+
function SkillView() {
|
|
244
|
+
useLocale();
|
|
245
|
+
const [items, setItems] = (0, import_react.useState)(null);
|
|
246
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
247
|
+
const [busy, setBusy] = (0, import_react.useState)(null);
|
|
248
|
+
const load = (0, import_react.useCallback)(() => {
|
|
249
|
+
void rpc("listSkills").then(
|
|
250
|
+
(v) => {
|
|
251
|
+
setError(null);
|
|
252
|
+
setItems(v);
|
|
253
|
+
},
|
|
254
|
+
(e) => {
|
|
255
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
256
|
+
}
|
|
257
|
+
);
|
|
258
|
+
}, []);
|
|
259
|
+
(0, import_react.useEffect)(() => {
|
|
260
|
+
load();
|
|
261
|
+
}, [load]);
|
|
262
|
+
if (error !== null) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "smc-sub", children: t("loadFailed", { e: error }) });
|
|
263
|
+
if (items === null) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "smc-sub", children: t("loading") });
|
|
264
|
+
if (items.length === 0) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "smc-sub", children: t("noSkills") });
|
|
265
|
+
const toggle = (s) => {
|
|
266
|
+
setBusy(s.path);
|
|
267
|
+
void rpc("toggleSkill", { path: s.path }).then(
|
|
268
|
+
() => {
|
|
269
|
+
setBusy(null);
|
|
270
|
+
load();
|
|
271
|
+
showToast(t("skillToggled", { name: s.name }));
|
|
272
|
+
},
|
|
273
|
+
(e) => {
|
|
274
|
+
setBusy(null);
|
|
275
|
+
showToast(e instanceof Error ? e.message : String(e), "error");
|
|
276
|
+
}
|
|
277
|
+
);
|
|
278
|
+
};
|
|
279
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: items.map((s) => {
|
|
280
|
+
const g = SOURCE_GROUP[s.source] ?? { label: s.source, cls: "" };
|
|
281
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-card", children: [
|
|
282
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-row", children: [
|
|
283
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-name", children: s.name }),
|
|
284
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `smc-badge ${g.cls}`, children: t(g.label) }),
|
|
285
|
+
!s.modelInvocable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-badge disabled", children: t("modelDisabled") }),
|
|
286
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-spacer" }),
|
|
287
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
288
|
+
"button",
|
|
289
|
+
{
|
|
290
|
+
type: "button",
|
|
291
|
+
className: `smc-toggle${s.writable && s.modelInvocable ? " on" : ""}`,
|
|
292
|
+
disabled: !s.writable || busy === s.name,
|
|
293
|
+
title: s.writable ? t("toggleModelVisible") : t("skillReadonly"),
|
|
294
|
+
onClick: () => {
|
|
295
|
+
toggle(s);
|
|
296
|
+
},
|
|
297
|
+
"aria-label": s.modelInvocable ? t("disable") : t("enable")
|
|
298
|
+
}
|
|
299
|
+
)
|
|
300
|
+
] }),
|
|
301
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-desc", children: s.description }),
|
|
302
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-meta", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-badge", children: t("provider", { name: s.provider }) }) })
|
|
303
|
+
] }, s.name);
|
|
304
|
+
}) });
|
|
305
|
+
}
|
|
306
|
+
function McpView() {
|
|
307
|
+
useLocale();
|
|
308
|
+
const [items, setItems] = (0, import_react.useState)(null);
|
|
309
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
310
|
+
const [editing, setEditing] = (0, import_react.useState)(null);
|
|
311
|
+
const load = (0, import_react.useCallback)(() => {
|
|
312
|
+
void rpc("listMcpServers").then(
|
|
313
|
+
(v) => {
|
|
314
|
+
setError(null);
|
|
315
|
+
setItems(v);
|
|
316
|
+
},
|
|
317
|
+
(e) => {
|
|
318
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
319
|
+
}
|
|
320
|
+
);
|
|
321
|
+
}, []);
|
|
322
|
+
(0, import_react.useEffect)(() => {
|
|
323
|
+
load();
|
|
324
|
+
}, [load]);
|
|
325
|
+
if (error !== null) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "smc-sub", children: t("loadFailed", { e: error }) });
|
|
326
|
+
if (items === null) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "smc-sub", children: t("loading") });
|
|
327
|
+
const toggle = (s) => {
|
|
328
|
+
void rpc("setMcpServerEnabled", { id: s.id, enabled: s.disabled }).then(
|
|
329
|
+
() => {
|
|
330
|
+
load();
|
|
331
|
+
showToast(t(s.disabled ? "enabledToast" : "disabledToast", { name: s.serverName }));
|
|
332
|
+
},
|
|
333
|
+
(e) => {
|
|
334
|
+
showToast(e instanceof Error ? e.message : String(e), "error");
|
|
335
|
+
}
|
|
336
|
+
);
|
|
337
|
+
};
|
|
338
|
+
const remove = (s) => {
|
|
339
|
+
void rpc("removeMcpServer", { id: s.id }).then(
|
|
340
|
+
() => {
|
|
341
|
+
load();
|
|
342
|
+
showToast(t("removed", { name: s.serverName }));
|
|
343
|
+
},
|
|
344
|
+
(e) => {
|
|
345
|
+
showToast(e instanceof Error ? e.message : String(e), "error");
|
|
346
|
+
}
|
|
347
|
+
);
|
|
348
|
+
};
|
|
349
|
+
const dotCls = (s) => s.fiberPhase === "failed" ? " failed" : s.disabled ? " idle" : "";
|
|
350
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
|
|
351
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-row", style: { marginBottom: 12 }, children: [
|
|
352
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "smc-btn primary", onClick: () => {
|
|
353
|
+
setEditing("new");
|
|
354
|
+
}, children: t("addServer") }),
|
|
355
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-spacer" }),
|
|
356
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-sub", children: t("hotApplied") })
|
|
357
|
+
] }),
|
|
358
|
+
editing !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ServerForm, { server: editing === "new" ? null : editing, onClose: () => {
|
|
359
|
+
setEditing(null);
|
|
360
|
+
}, onSaved: () => {
|
|
361
|
+
setEditing(null);
|
|
362
|
+
load();
|
|
363
|
+
} }),
|
|
364
|
+
items.map((s) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-card", children: [
|
|
365
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-row", children: [
|
|
366
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-name", children: s.serverName }),
|
|
367
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-badge", children: s.transport }),
|
|
368
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `smc-dot${dotCls(s)}` }),
|
|
369
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-spacer" }),
|
|
370
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "smc-btn", onClick: () => {
|
|
371
|
+
setEditing(s);
|
|
372
|
+
}, children: t("edit") }),
|
|
373
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "smc-btn danger", onClick: () => {
|
|
374
|
+
remove(s);
|
|
375
|
+
}, children: t("remove") }),
|
|
376
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
377
|
+
"button",
|
|
378
|
+
{
|
|
379
|
+
type: "button",
|
|
380
|
+
className: `smc-toggle${!s.disabled ? " on" : ""}`,
|
|
381
|
+
title: s.disabled ? t("enableServer") : t("disableServer"),
|
|
382
|
+
onClick: () => {
|
|
383
|
+
toggle(s);
|
|
384
|
+
},
|
|
385
|
+
"aria-label": s.disabled ? t("enable") : t("disable")
|
|
386
|
+
}
|
|
387
|
+
)
|
|
388
|
+
] }),
|
|
389
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-desc", children: s.transport === "stdio" ? `${s.command ?? ""} ${(s.args ?? []).join(" ")}` : s.url })
|
|
390
|
+
] }, s.id))
|
|
391
|
+
] });
|
|
392
|
+
}
|
|
393
|
+
function ServerForm({ server, onClose, onSaved }) {
|
|
394
|
+
useLocale();
|
|
395
|
+
const [name, setName] = (0, import_react.useState)(server?.serverName ?? "");
|
|
396
|
+
const [transport, setTransport] = (0, import_react.useState)(server?.transport ?? "stdio");
|
|
397
|
+
const [command, setCommand] = (0, import_react.useState)(server?.command ?? "");
|
|
398
|
+
const [args, setArgs] = (0, import_react.useState)((server?.args ?? []).join(" "));
|
|
399
|
+
const [cwd, setCwd] = (0, import_react.useState)(server?.cwd ?? "");
|
|
400
|
+
const [url, setUrl] = (0, import_react.useState)(server?.url ?? "");
|
|
401
|
+
const [error, setError] = (0, import_react.useState)("");
|
|
402
|
+
const save = () => {
|
|
403
|
+
if (!/^[A-Za-z0-9_-]{1,32}$/.test(name)) {
|
|
404
|
+
setError(t("serverNameInvalid"));
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
const config = transport === "stdio" ? { serverName: name, transport, command, args: args.split(/\s+/).filter(Boolean), cwd } : { serverName: name, transport, url };
|
|
408
|
+
const call = server === null ? rpc("createMcpServer", { config }) : rpc("updateMcpServer", { id: server.id, config });
|
|
409
|
+
void call.then(
|
|
410
|
+
() => {
|
|
411
|
+
onSaved();
|
|
412
|
+
showToast(server === null ? t("added", { name }) : t("updated", { name }));
|
|
413
|
+
},
|
|
414
|
+
(e) => {
|
|
415
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
416
|
+
}
|
|
417
|
+
);
|
|
418
|
+
};
|
|
419
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-form", children: [
|
|
420
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-field", children: [
|
|
421
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-label", children: t("serverNameLabel") }),
|
|
422
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { className: "smc-input", value: name, onChange: (e) => {
|
|
423
|
+
setName(e.target.value);
|
|
424
|
+
}, placeholder: "filesystem" })
|
|
425
|
+
] }),
|
|
426
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-field", children: [
|
|
427
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-label", children: t("transport") }),
|
|
428
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", { className: "smc-select", value: transport, onChange: (e) => {
|
|
429
|
+
setTransport(e.target.value);
|
|
430
|
+
}, children: [
|
|
431
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "stdio", children: "stdio" }),
|
|
432
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "streamable-http", children: "streamable-http" })
|
|
433
|
+
] })
|
|
434
|
+
] }),
|
|
435
|
+
transport === "stdio" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
436
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-field", children: [
|
|
437
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-label", children: t("command") }),
|
|
438
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { className: "smc-input", value: command, onChange: (e) => {
|
|
439
|
+
setCommand(e.target.value);
|
|
440
|
+
}, placeholder: "npx" })
|
|
441
|
+
] }),
|
|
442
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-field", children: [
|
|
443
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-label", children: t("argsLabel") }),
|
|
444
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { className: "smc-input", value: args, onChange: (e) => {
|
|
445
|
+
setArgs(e.target.value);
|
|
446
|
+
}, placeholder: "-y @modelcontextprotocol/server-filesystem" })
|
|
447
|
+
] }),
|
|
448
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-field", children: [
|
|
449
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-label", children: t("cwdLabel") }),
|
|
450
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { className: "smc-input", value: cwd, onChange: (e) => {
|
|
451
|
+
setCwd(e.target.value);
|
|
452
|
+
}, placeholder: "" })
|
|
453
|
+
] })
|
|
454
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-field", children: [
|
|
455
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-label", children: t("url") }),
|
|
456
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { className: "smc-input", value: url, onChange: (e) => {
|
|
457
|
+
setUrl(e.target.value);
|
|
458
|
+
}, placeholder: "https://mcp.example.com/xxx" })
|
|
459
|
+
] }),
|
|
460
|
+
error !== "" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-error", children: error }),
|
|
461
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-row", style: { justifyContent: "flex-end", gap: 8, marginTop: 8 }, children: [
|
|
462
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "smc-btn", onClick: onClose, children: t("cancel") }),
|
|
463
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "smc-btn primary", onClick: save, children: server === null ? t("add") : t("save") })
|
|
464
|
+
] })
|
|
465
|
+
] });
|
|
466
|
+
}
|
|
467
|
+
function CenterPanel() {
|
|
468
|
+
useLocale();
|
|
469
|
+
const [view, setView] = (0, import_react.useState)("skill");
|
|
470
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", flexDirection: "column", height: "100%", minHeight: 0 }, children: [
|
|
471
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { flex: "none" }, children: [
|
|
472
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-title", children: "Skill & MCP" }),
|
|
473
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-sub", children: t("sectionSub") }),
|
|
474
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-tabs", children: [
|
|
475
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: `smc-tab${view === "skill" ? " active" : ""}`, onClick: () => {
|
|
476
|
+
setView("skill");
|
|
477
|
+
}, children: "Skill" }),
|
|
478
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: `smc-tab${view === "mcp" ? " active" : ""}`, onClick: () => {
|
|
479
|
+
setView("mcp");
|
|
480
|
+
}, children: "MCP" })
|
|
481
|
+
] })
|
|
482
|
+
] }),
|
|
483
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { flex: 1, minHeight: 0, overflow: "auto" }, children: view === "skill" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SkillView, {}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(McpView, {}) })
|
|
484
|
+
] });
|
|
485
|
+
}
|
|
486
|
+
function McpSidebarTab({ visible }) {
|
|
487
|
+
useLocale();
|
|
488
|
+
const [items, setItems] = (0, import_react.useState)([]);
|
|
489
|
+
(0, import_react.useEffect)(() => {
|
|
490
|
+
if (!visible) return;
|
|
491
|
+
const tick = () => {
|
|
492
|
+
void rpc("mcpStatus").then((v) => {
|
|
493
|
+
setItems(v);
|
|
494
|
+
}).catch(() => {
|
|
495
|
+
});
|
|
496
|
+
};
|
|
497
|
+
tick();
|
|
498
|
+
const timer = setInterval(tick, 1e3);
|
|
499
|
+
return () => {
|
|
500
|
+
clearInterval(timer);
|
|
501
|
+
};
|
|
502
|
+
}, [visible]);
|
|
503
|
+
if (items.length === 0) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-empty", children: t("noMcpServer") });
|
|
504
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-sidebar", children: items.map((s) => {
|
|
505
|
+
const dotCls = s.fiberPhase === "failed" ? " failed" : s.connected ? "" : " idle";
|
|
506
|
+
const state = s.fiberPhase === "failed" ? t("failed") : s.connected ? t("connected") : t("notSynced");
|
|
507
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
|
|
508
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-srv", title: `fiber ${s.fiberPhase ?? "?"} \xB7 ${s.toolCount} tools (${s.statusSource})`, children: [
|
|
509
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `smc-dot${dotCls}` }),
|
|
510
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-srv-name", children: s.serverName }),
|
|
511
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "smc-srv-count", children: [
|
|
512
|
+
s.toolCount,
|
|
513
|
+
" tools"
|
|
514
|
+
] })
|
|
515
|
+
] }),
|
|
516
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-srv-state", children: state })
|
|
517
|
+
] }, s.serverName);
|
|
518
|
+
}) });
|
|
519
|
+
}
|
|
520
|
+
function Toast() {
|
|
521
|
+
const toast = useToast();
|
|
522
|
+
if (toast === null) return null;
|
|
523
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: `smc-toast ${toast.kind}`, children: toast.message });
|
|
524
|
+
}
|
|
525
|
+
function SidebarSkillTab({ visible, cwd }) {
|
|
526
|
+
useLocale();
|
|
527
|
+
const [items, setItems] = (0, import_react.useState)([]);
|
|
528
|
+
const load = (0, import_react.useCallback)(() => {
|
|
529
|
+
void rpc("listSkills", { cwd }).then(
|
|
530
|
+
(v) => {
|
|
531
|
+
setItems(v);
|
|
532
|
+
},
|
|
533
|
+
() => {
|
|
534
|
+
setItems([]);
|
|
535
|
+
}
|
|
536
|
+
);
|
|
537
|
+
}, [cwd]);
|
|
538
|
+
(0, import_react.useEffect)(() => {
|
|
539
|
+
if (!visible) return;
|
|
540
|
+
load();
|
|
541
|
+
}, [visible, load]);
|
|
542
|
+
if (items.length === 0) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-empty", children: t("noSkills") });
|
|
543
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-sidebar", children: items.map((s) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-srv", title: `${s.description}
|
|
544
|
+
${s.path}`, children: [
|
|
545
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `smc-dot${s.modelInvocable ? "" : " idle"}` }),
|
|
546
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-srv-name", children: s.name }),
|
|
547
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
548
|
+
"button",
|
|
549
|
+
{
|
|
550
|
+
type: "button",
|
|
551
|
+
className: `smc-toggle${s.modelInvocable ? " on" : ""}`,
|
|
552
|
+
title: t("toggleModelVisible"),
|
|
553
|
+
onClick: () => {
|
|
554
|
+
void rpc("toggleSkill", { path: s.path }).then(() => {
|
|
555
|
+
load();
|
|
556
|
+
});
|
|
557
|
+
},
|
|
558
|
+
"aria-label": s.modelInvocable ? t("disable") : t("enable")
|
|
559
|
+
}
|
|
560
|
+
)
|
|
561
|
+
] }, s.path)) });
|
|
562
|
+
}
|
|
563
|
+
var inject = ["slots", "connection", "locale"];
|
|
564
|
+
function apply(ctx) {
|
|
565
|
+
injectCss();
|
|
566
|
+
rpc = async (endpoint, payload = {}) => {
|
|
567
|
+
const result = await ctx.connection.rpc.call("/skill-mcp", endpoint, payload);
|
|
568
|
+
if (result.ok) return result.value;
|
|
569
|
+
throw new Error(result.error?.message ?? `skill-mcp-center: ${endpoint} failed`);
|
|
570
|
+
};
|
|
571
|
+
ctx.effect(() => {
|
|
572
|
+
const d1 = ctx.locale.register(LOCALE_NS, "zh", zhDict);
|
|
573
|
+
const d2 = ctx.locale.register(LOCALE_NS, "en", enDict);
|
|
574
|
+
return () => {
|
|
575
|
+
d1();
|
|
576
|
+
d2();
|
|
577
|
+
};
|
|
578
|
+
});
|
|
579
|
+
t = ctx.locale.bind(LOCALE_NS);
|
|
580
|
+
ctx.effect(() => ctx.locale.subscribe(() => {
|
|
581
|
+
localeRevision += 1;
|
|
582
|
+
localeListeners.forEach((l) => l());
|
|
583
|
+
}));
|
|
584
|
+
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
585
|
+
name: "settings.section",
|
|
586
|
+
id: "skill-mcp-center",
|
|
587
|
+
order: 60,
|
|
588
|
+
label: () => "Skill & MCP"
|
|
589
|
+
}, CenterPanel));
|
|
590
|
+
ctx.inject(["betterSidebar"], (sidebarCtx) => {
|
|
591
|
+
const service = sidebarCtx.betterSidebar;
|
|
592
|
+
if (service === void 0) return;
|
|
593
|
+
sidebarCtx.effect(() => service.registerTab({
|
|
594
|
+
id: "@max-null/dsh-skill-mcp-center:mcp",
|
|
595
|
+
title: () => "MCP",
|
|
596
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { width: "15", height: "15", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
597
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: "2", y: "2", width: "20", height: "8", rx: "2", ry: "2" }),
|
|
598
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: "2", y: "14", width: "20", height: "8", rx: "2", ry: "2" }),
|
|
599
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "6", y1: "6", x2: "6.01", y2: "6" }),
|
|
600
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "6", y1: "18", x2: "6.01", y2: "18" })
|
|
601
|
+
] }),
|
|
602
|
+
order: 70,
|
|
603
|
+
single: true,
|
|
604
|
+
component: (props) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(McpSidebarTab, { visible: props.visible })
|
|
605
|
+
}));
|
|
606
|
+
sidebarCtx.effect(() => service.registerTab({
|
|
607
|
+
id: "@max-null/dsh-skill-mcp-center:skills",
|
|
608
|
+
title: () => "Skill",
|
|
609
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { width: "15", height: "15", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", 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" }) }),
|
|
610
|
+
order: 71,
|
|
611
|
+
single: true,
|
|
612
|
+
component: (props) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SidebarSkillTab, { visible: props.visible, cwd: props.scope?.cwd })
|
|
613
|
+
}));
|
|
614
|
+
});
|
|
615
|
+
ctx.slots.inject("shell.overlay", () => ctx.slots.register({
|
|
616
|
+
name: "shell.overlay",
|
|
617
|
+
id: "skill-mcp-center-toast",
|
|
618
|
+
order: 90
|
|
619
|
+
}, Toast));
|
|
620
|
+
}
|
|
621
|
+
return module.exports;
|
|
622
|
+
},
|
|
623
|
+
});
|
|
624
|
+
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# dsh-skill-mcp-center bundle patch: one insert over the profile root.
|
|
2
|
+
# Host half reads ctx.skills (skill catalog) + ctx.loader/ctx.tools (MCP);
|
|
3
|
+
# client half registers a Settings section plus an optional sidebar MCP tab.
|
|
4
|
+
- insert:
|
|
5
|
+
- id: skill-mcp-center
|
|
6
|
+
name: '@max-null/dsh-skill-mcp-center'
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Toggle `disable-model-invocation` in SKILL.md text; creates frontmatter if absent. */
|
|
2
|
+
export declare function setDisableModelInvocation(text: string, disabled: boolean): string;
|
|
3
|
+
/** Parsed SKILL.md metadata relevant to the management surface. */
|
|
4
|
+
export interface SkillFrontmatter {
|
|
5
|
+
name: string;
|
|
6
|
+
description: string;
|
|
7
|
+
modelInvocable: boolean;
|
|
8
|
+
}
|
|
9
|
+
/** Parse a SKILL.md frontmatter block; null when absent or missing a name. */
|
|
10
|
+
export declare function parseSkillFrontmatter(text: string): SkillFrontmatter | null;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal SKILL.md frontmatter editing and parsing for `disable-model-invocation`.
|
|
3
|
+
* Text-level editing (not a full YAML round-trip) so unrelated frontmatter keys,
|
|
4
|
+
* comments, and formatting survive untouched.
|
|
5
|
+
*/
|
|
6
|
+
import { load } from 'js-yaml';
|
|
7
|
+
/** Toggle `disable-model-invocation` in SKILL.md text; creates frontmatter if absent. */
|
|
8
|
+
export function setDisableModelInvocation(text, disabled) {
|
|
9
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text);
|
|
10
|
+
if (match === null) {
|
|
11
|
+
return `---\ndisable-model-invocation: ${disabled}\n---\n\n${text}`;
|
|
12
|
+
}
|
|
13
|
+
const full = match[0];
|
|
14
|
+
const body = match[1];
|
|
15
|
+
const line = /^(\s*)disable-model-invocation\s*:.*$/m;
|
|
16
|
+
const next = line.test(body)
|
|
17
|
+
? body.replace(line, `$1disable-model-invocation: ${disabled}`)
|
|
18
|
+
: body.replace(/\s*$/, `\ndisable-model-invocation: ${disabled}`);
|
|
19
|
+
return text.replace(full, full.replace(body, next));
|
|
20
|
+
}
|
|
21
|
+
/** Parse a SKILL.md frontmatter block; null when absent or missing a name. */
|
|
22
|
+
export function parseSkillFrontmatter(text) {
|
|
23
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text);
|
|
24
|
+
if (match === null)
|
|
25
|
+
return null;
|
|
26
|
+
try {
|
|
27
|
+
const data = load(match[1]);
|
|
28
|
+
if (typeof data.name !== 'string' || data.name.trim() === '')
|
|
29
|
+
return null;
|
|
30
|
+
return {
|
|
31
|
+
name: data.name,
|
|
32
|
+
description: typeof data.description === 'string' ? data.description : '',
|
|
33
|
+
modelInvocable: data['disable-model-invocation'] !== true,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dsh-skill-mcp-center` host half: mounts the skill/MCP engine and its
|
|
3
|
+
* loopback RPC. The browser half (`./client`) is picked up through the
|
|
4
|
+
* package's `dsh.client` declaration.
|
|
5
|
+
*/
|
|
6
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
7
|
+
export { SkillMcpService } from './service.ts';
|
|
8
|
+
export type { McpConfig, McpServer, McpServerStatus, SkillView } from './service.ts';
|
|
9
|
+
export declare const name = "dsh-skill-mcp-center";
|
|
10
|
+
/** The engine declares its own loader/tools dependencies. */
|
|
11
|
+
export declare const inject: string[];
|
|
12
|
+
export declare function apply(ctx: Context): Promise<void>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { SkillMcpService } from "./service.js";
|
|
2
|
+
import { SkillMcpRpc } from "./rpc.js";
|
|
3
|
+
export { SkillMcpService } from "./service.js";
|
|
4
|
+
export const name = 'dsh-skill-mcp-center';
|
|
5
|
+
/** The engine declares its own loader/tools dependencies. */
|
|
6
|
+
export const inject = ['loader', 'tools'];
|
|
7
|
+
export async function apply(ctx) {
|
|
8
|
+
await ctx.plugin(SkillMcpService);
|
|
9
|
+
await ctx.plugin(SkillMcpRpc);
|
|
10
|
+
}
|
package/dist/rpc.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `SkillMcpRpc` — a private loopback RPC channel exposing the engine to the
|
|
3
|
+
* browser half (same seam the plugin center uses; the Typert Remote path is
|
|
4
|
+
* closed to third parties).
|
|
5
|
+
*/
|
|
6
|
+
import { Service, type Context } from '@deepseek-ai/cordis';
|
|
7
|
+
export declare class SkillMcpRpc extends Service {
|
|
8
|
+
static inject: string[];
|
|
9
|
+
constructor(ctx: Context);
|
|
10
|
+
}
|
|
11
|
+
export default SkillMcpRpc;
|
package/dist/rpc.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `SkillMcpRpc` — a private loopback RPC channel exposing the engine to the
|
|
3
|
+
* browser half (same seam the plugin center uses; the Typert Remote path is
|
|
4
|
+
* closed to third parties).
|
|
5
|
+
*/
|
|
6
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
7
|
+
const CHANNEL = '/skill-mcp';
|
|
8
|
+
function internal(message) {
|
|
9
|
+
return { ok: false, error: { code: 'internal', message, details: {} } };
|
|
10
|
+
}
|
|
11
|
+
export class SkillMcpRpc extends Service {
|
|
12
|
+
static inject = ['skillMcp', 'connection'];
|
|
13
|
+
constructor(ctx) {
|
|
14
|
+
super(ctx, 'skillMcpRpc');
|
|
15
|
+
ctx.connection.rpc.handle(CHANNEL, async (endpoint, payload) => {
|
|
16
|
+
try {
|
|
17
|
+
const p = (payload ?? {});
|
|
18
|
+
switch (endpoint) {
|
|
19
|
+
case 'listSkills': {
|
|
20
|
+
const cwd = p.cwd;
|
|
21
|
+
return { ok: true, value: await ctx.skillMcp.listSkills(typeof cwd === 'string' ? cwd : undefined) };
|
|
22
|
+
}
|
|
23
|
+
case 'toggleSkill': {
|
|
24
|
+
const path = p.path;
|
|
25
|
+
if (typeof path !== 'string' || path === '')
|
|
26
|
+
return internal('toggleSkill: path is required');
|
|
27
|
+
return { ok: true, value: await ctx.skillMcp.toggleSkill(path) };
|
|
28
|
+
}
|
|
29
|
+
case 'listMcpServers':
|
|
30
|
+
return { ok: true, value: await ctx.skillMcp.listMcpServers() };
|
|
31
|
+
case 'createMcpServer':
|
|
32
|
+
return { ok: true, value: await ctx.skillMcp.createMcpServer(p.config) };
|
|
33
|
+
case 'updateMcpServer':
|
|
34
|
+
return { ok: true, value: await ctx.skillMcp.updateMcpServer(String(p.id), p.config) };
|
|
35
|
+
case 'removeMcpServer':
|
|
36
|
+
return { ok: true, value: await ctx.skillMcp.removeMcpServer(String(p.id)) };
|
|
37
|
+
case 'setMcpServerEnabled':
|
|
38
|
+
return { ok: true, value: await ctx.skillMcp.setMcpServerEnabled(String(p.id), p.enabled === true) };
|
|
39
|
+
case 'mcpStatus':
|
|
40
|
+
return { ok: true, value: await ctx.skillMcp.mcpStatus() };
|
|
41
|
+
default:
|
|
42
|
+
return internal(`unknown endpoint "${endpoint}"`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
return internal(error instanceof Error ? error.message : String(error));
|
|
47
|
+
}
|
|
48
|
+
}, { authority: 'loopback' });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export default SkillMcpRpc;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `SkillMcpService` — the process-local composition of skill and MCP
|
|
3
|
+
* management. Skills are read straight off disk (host-level skill-filesystem
|
|
4
|
+
* is disabled in web-app — presets own discovery — so `ctx.skills` has no
|
|
5
|
+
* global layer to list); MCP servers are the `mcp-client` loader entries,
|
|
6
|
+
* managed hot through `ctx.loader` and observed through a feature-detected
|
|
7
|
+
* `ctx.mcpStatus` seam with a derived fallback.
|
|
8
|
+
*/
|
|
9
|
+
import { Service, type Context } from '@deepseek-ai/cordis';
|
|
10
|
+
declare module '@deepseek-ai/cordis' {
|
|
11
|
+
interface Context {
|
|
12
|
+
/** The skill/MCP center engine (provided by this package's host half). */
|
|
13
|
+
skillMcp: SkillMcpService;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** One skill as the Settings surface exposes it. */
|
|
17
|
+
export interface SkillView {
|
|
18
|
+
name: string;
|
|
19
|
+
description: string;
|
|
20
|
+
source: string;
|
|
21
|
+
provider: string;
|
|
22
|
+
modelInvocable: boolean;
|
|
23
|
+
userInvocable: boolean;
|
|
24
|
+
/** Always true — user-level skills are disk-backed and toggleable. */
|
|
25
|
+
writable: boolean;
|
|
26
|
+
/** Absolute SKILL.md path (the toggle target; opaque to the client display). */
|
|
27
|
+
path: string;
|
|
28
|
+
}
|
|
29
|
+
/** One MCP server as the Settings surface exposes it. */
|
|
30
|
+
export interface McpServer {
|
|
31
|
+
id: string;
|
|
32
|
+
serverName: string;
|
|
33
|
+
transport: 'stdio' | 'streamable-http';
|
|
34
|
+
command?: string;
|
|
35
|
+
args?: string[];
|
|
36
|
+
cwd?: string;
|
|
37
|
+
url?: string;
|
|
38
|
+
headers?: Record<string, string>;
|
|
39
|
+
disabled: boolean;
|
|
40
|
+
fiberPhase: string | null;
|
|
41
|
+
}
|
|
42
|
+
/** Runtime status of one MCP server (sidebar polling). */
|
|
43
|
+
export interface McpServerStatus {
|
|
44
|
+
serverName: string;
|
|
45
|
+
fiberPhase: string | null;
|
|
46
|
+
toolCount: number;
|
|
47
|
+
connected: boolean;
|
|
48
|
+
statusSource: 'seam' | 'derived';
|
|
49
|
+
}
|
|
50
|
+
/** Client-supplied MCP server config, normalized to the mcp-client shape. */
|
|
51
|
+
export interface McpConfig {
|
|
52
|
+
serverName: string;
|
|
53
|
+
transport: 'stdio' | 'streamable-http';
|
|
54
|
+
command?: string;
|
|
55
|
+
args?: string[];
|
|
56
|
+
cwd?: string;
|
|
57
|
+
url?: string;
|
|
58
|
+
headers?: Record<string, string>;
|
|
59
|
+
}
|
|
60
|
+
export declare class SkillMcpService extends Service {
|
|
61
|
+
static inject: string[];
|
|
62
|
+
constructor(ctx: Context);
|
|
63
|
+
/** User-level skills plus, when a workspace is given, its project-level skills. */
|
|
64
|
+
listSkills(cwd?: string): Promise<SkillView[]>;
|
|
65
|
+
/** Flip one disk-backed skill's model invocation by rewriting its SKILL.md frontmatter. */
|
|
66
|
+
toggleSkill(path: string): Promise<SkillView>;
|
|
67
|
+
/** Every `mcp-client` loader entry as a server card. */
|
|
68
|
+
listMcpServers(): Promise<McpServer[]>;
|
|
69
|
+
/** Add one mcp-client entry — hot-connects (create → init) and persists. */
|
|
70
|
+
createMcpServer(config: McpConfig): Promise<{
|
|
71
|
+
id: string;
|
|
72
|
+
}>;
|
|
73
|
+
/** Rewrite one server's config — hot-updates the running fiber. */
|
|
74
|
+
updateMcpServer(id: string, config: McpConfig): Promise<void>;
|
|
75
|
+
/** Remove one server — disconnects and unregisters its tools. */
|
|
76
|
+
removeMcpServer(id: string): Promise<void>;
|
|
77
|
+
/** Enable/disable one server without deleting its config. */
|
|
78
|
+
setMcpServerEnabled(id: string, enabled: boolean): Promise<void>;
|
|
79
|
+
/** Runtime status per server: upstream `mcpStatus` seam when present, else derived. */
|
|
80
|
+
mcpStatus(): Promise<McpServerStatus[]>;
|
|
81
|
+
}
|
|
82
|
+
export default SkillMcpService;
|
package/dist/service.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `SkillMcpService` — the process-local composition of skill and MCP
|
|
3
|
+
* management. Skills are read straight off disk (host-level skill-filesystem
|
|
4
|
+
* is disabled in web-app — presets own discovery — so `ctx.skills` has no
|
|
5
|
+
* global layer to list); MCP servers are the `mcp-client` loader entries,
|
|
6
|
+
* managed hot through `ctx.loader` and observed through a feature-detected
|
|
7
|
+
* `ctx.mcpStatus` seam with a derived fallback.
|
|
8
|
+
*/
|
|
9
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
10
|
+
import { readdir, readFile, writeFile } from 'node:fs/promises';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { parseSkillFrontmatter, setDisableModelInvocation } from "./frontmatter.js";
|
|
14
|
+
/** Runtime mirror of cordis FiberState (a cross-package const enum). */
|
|
15
|
+
const FIBER_PHASE = {
|
|
16
|
+
0: 'pending',
|
|
17
|
+
1: 'loading',
|
|
18
|
+
2: 'active',
|
|
19
|
+
3: 'failed',
|
|
20
|
+
4: null,
|
|
21
|
+
5: 'unloading',
|
|
22
|
+
};
|
|
23
|
+
/** Normalize a client MCP config into the full mcp-client config (defaults filled). */
|
|
24
|
+
function fullMcpConfig(input) {
|
|
25
|
+
if (input.transport === 'stdio') {
|
|
26
|
+
return {
|
|
27
|
+
transport: 'stdio',
|
|
28
|
+
serverName: input.serverName,
|
|
29
|
+
command: input.command ?? '',
|
|
30
|
+
args: input.args ?? [],
|
|
31
|
+
env: {},
|
|
32
|
+
cwd: input.cwd ?? '',
|
|
33
|
+
toolCallTimeoutMs: 60_000,
|
|
34
|
+
failOnStartupError: false,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
transport: 'streamable-http',
|
|
39
|
+
serverName: input.serverName,
|
|
40
|
+
url: input.url ?? '',
|
|
41
|
+
headers: input.headers ?? {},
|
|
42
|
+
toolCallTimeoutMs: 60_000,
|
|
43
|
+
failOnStartupError: false,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** User-level skill roots (host-level filesystem discovery is preset-owned in web). */
|
|
47
|
+
const SKILL_ROOTS = [
|
|
48
|
+
{ path: join(homedir(), '.dsh', 'skills'), source: 'user-dsh' },
|
|
49
|
+
{ path: join(homedir(), '.agents', 'skills'), source: 'user-agents' },
|
|
50
|
+
];
|
|
51
|
+
/** Absolute SKILL.md path for one directory/file entry, or null. */
|
|
52
|
+
function skillPathFor(root, name, isDirectory) {
|
|
53
|
+
if (isDirectory)
|
|
54
|
+
return join(root, name, 'SKILL.md');
|
|
55
|
+
if (name.endsWith('.md'))
|
|
56
|
+
return join(root, name);
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
/** Scan one root for SKILL.md entries and parse their frontmatter. */
|
|
60
|
+
async function scanSkillRoot(root, source) {
|
|
61
|
+
let entries;
|
|
62
|
+
try {
|
|
63
|
+
entries = await readdir(root, { withFileTypes: true });
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return []; // root absent
|
|
67
|
+
}
|
|
68
|
+
const skills = [];
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
const skillPath = skillPathFor(root, entry.name, entry.isDirectory());
|
|
71
|
+
if (skillPath === null)
|
|
72
|
+
continue;
|
|
73
|
+
let text;
|
|
74
|
+
try {
|
|
75
|
+
text = await readFile(skillPath, 'utf8');
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const fm = parseSkillFrontmatter(text);
|
|
81
|
+
if (fm === null)
|
|
82
|
+
continue;
|
|
83
|
+
skills.push({
|
|
84
|
+
name: fm.name,
|
|
85
|
+
description: fm.description,
|
|
86
|
+
source,
|
|
87
|
+
provider: 'filesystem',
|
|
88
|
+
modelInvocable: fm.modelInvocable,
|
|
89
|
+
userInvocable: true,
|
|
90
|
+
writable: true,
|
|
91
|
+
path: skillPath,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return skills;
|
|
95
|
+
}
|
|
96
|
+
export class SkillMcpService extends Service {
|
|
97
|
+
static inject = ['loader', 'tools'];
|
|
98
|
+
constructor(ctx) {
|
|
99
|
+
super(ctx, 'skillMcp');
|
|
100
|
+
}
|
|
101
|
+
/** User-level skills plus, when a workspace is given, its project-level skills. */
|
|
102
|
+
async listSkills(cwd) {
|
|
103
|
+
const skills = [];
|
|
104
|
+
for (const root of SKILL_ROOTS) {
|
|
105
|
+
skills.push(...await scanSkillRoot(root.path, root.source));
|
|
106
|
+
}
|
|
107
|
+
if (cwd !== undefined && cwd !== '') {
|
|
108
|
+
skills.push(...await scanSkillRoot(join(cwd, '.agents', 'skills'), 'project-agents'));
|
|
109
|
+
skills.push(...await scanSkillRoot(join(cwd, '.dsh', 'skills'), 'project-dsh'));
|
|
110
|
+
}
|
|
111
|
+
return skills;
|
|
112
|
+
}
|
|
113
|
+
/** Flip one disk-backed skill's model invocation by rewriting its SKILL.md frontmatter. */
|
|
114
|
+
async toggleSkill(path) {
|
|
115
|
+
let text;
|
|
116
|
+
try {
|
|
117
|
+
text = await readFile(path, 'utf8');
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
throw new Error('skill-not-found');
|
|
121
|
+
}
|
|
122
|
+
const fm = parseSkillFrontmatter(text);
|
|
123
|
+
if (fm === null)
|
|
124
|
+
throw new Error('skill-not-found');
|
|
125
|
+
// Currently model-invocable → disable.
|
|
126
|
+
await writeFile(path, setDisableModelInvocation(text, fm.modelInvocable), 'utf8');
|
|
127
|
+
return {
|
|
128
|
+
name: fm.name,
|
|
129
|
+
description: fm.description,
|
|
130
|
+
source: 'user',
|
|
131
|
+
provider: 'filesystem',
|
|
132
|
+
modelInvocable: !fm.modelInvocable,
|
|
133
|
+
userInvocable: true,
|
|
134
|
+
writable: true,
|
|
135
|
+
path,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/** Every `mcp-client` loader entry as a server card. */
|
|
139
|
+
async listMcpServers() {
|
|
140
|
+
const servers = [];
|
|
141
|
+
for (const entry of this.ctx.loader.entries()) {
|
|
142
|
+
// The official bridge's specifier is the package name, not its internal
|
|
143
|
+
// plugin `name` ('mcp-client'); one entry = one MCP server.
|
|
144
|
+
if (entry.options.name !== '@deepseek-ai/dsh-mcp-client')
|
|
145
|
+
continue;
|
|
146
|
+
const cfg = entry.options.config;
|
|
147
|
+
servers.push({
|
|
148
|
+
id: entry.id,
|
|
149
|
+
serverName: cfg?.serverName ?? entry.id,
|
|
150
|
+
transport: cfg?.transport === 'streamable-http' ? 'streamable-http' : 'stdio',
|
|
151
|
+
command: cfg?.command,
|
|
152
|
+
args: cfg?.args,
|
|
153
|
+
cwd: cfg?.cwd,
|
|
154
|
+
url: cfg?.url,
|
|
155
|
+
headers: cfg?.headers,
|
|
156
|
+
disabled: entry.disabled,
|
|
157
|
+
fiberPhase: entry.fiber === undefined ? null : FIBER_PHASE[entry.fiber.state],
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return servers;
|
|
161
|
+
}
|
|
162
|
+
/** Add one mcp-client entry — hot-connects (create → init) and persists. */
|
|
163
|
+
async createMcpServer(config) {
|
|
164
|
+
const id = `mcp-${config.serverName}`;
|
|
165
|
+
// loader.create's runtime ensureId honors an explicit id (cordis.yml rows
|
|
166
|
+
// all carry one), but its d.ts Omit<EntryOptions,'id'> hides that — assert.
|
|
167
|
+
await this.ctx.loader.create({ id, name: '@deepseek-ai/dsh-mcp-client', config: fullMcpConfig(config) });
|
|
168
|
+
return { id };
|
|
169
|
+
}
|
|
170
|
+
/** Rewrite one server's config — hot-updates the running fiber. */
|
|
171
|
+
async updateMcpServer(id, config) {
|
|
172
|
+
await this.ctx.loader.update(id, { config: fullMcpConfig(config) });
|
|
173
|
+
}
|
|
174
|
+
/** Remove one server — disconnects and unregisters its tools. */
|
|
175
|
+
async removeMcpServer(id) {
|
|
176
|
+
await this.ctx.loader.remove(id);
|
|
177
|
+
}
|
|
178
|
+
/** Enable/disable one server without deleting its config. */
|
|
179
|
+
async setMcpServerEnabled(id, enabled) {
|
|
180
|
+
await this.ctx.loader.update(id, { disabled: enabled ? null : true });
|
|
181
|
+
}
|
|
182
|
+
/** Runtime status per server: upstream `mcpStatus` seam when present, else derived. */
|
|
183
|
+
async mcpStatus() {
|
|
184
|
+
const servers = await this.listMcpServers();
|
|
185
|
+
const seam = this.ctx.mcpStatus;
|
|
186
|
+
if (seam !== undefined) {
|
|
187
|
+
const byName = new Map(seam.list().map(s => [s.serverName, s]));
|
|
188
|
+
return servers.map(s => {
|
|
189
|
+
const st = byName.get(s.serverName);
|
|
190
|
+
return {
|
|
191
|
+
serverName: s.serverName,
|
|
192
|
+
fiberPhase: s.fiberPhase,
|
|
193
|
+
toolCount: st?.toolCount ?? 0,
|
|
194
|
+
connected: st?.phase === 'connected',
|
|
195
|
+
statusSource: 'seam',
|
|
196
|
+
};
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
const toolNames = this.ctx.tools.schemas().map(s => s.name);
|
|
200
|
+
return servers.map(s => {
|
|
201
|
+
const prefix = `mcp__${s.serverName}__`;
|
|
202
|
+
const toolCount = toolNames.filter(n => n.startsWith(prefix)).length;
|
|
203
|
+
return {
|
|
204
|
+
serverName: s.serverName,
|
|
205
|
+
fiberPhase: s.fiberPhase,
|
|
206
|
+
toolCount,
|
|
207
|
+
connected: !s.disabled && s.fiberPhase === 'active' && toolCount > 0,
|
|
208
|
+
statusSource: 'derived',
|
|
209
|
+
};
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
export default SkillMcpService;
|
package/package.json
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@max-null/dsh-skill-mcp-center",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Skill & MCP management center for DeepSeek Harness — manage skills and MCP servers in Settings, live MCP status in the sidebar",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "dist/index.js",
|
|
11
|
+
"types": "dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./client": "./client.js",
|
|
18
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
19
|
+
"./package.json": "./package.json"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"client.js",
|
|
24
|
+
"cordis.patch.yml"
|
|
25
|
+
],
|
|
26
|
+
"dsh": {
|
|
27
|
+
"bundle": {
|
|
28
|
+
"patch": "./cordis.patch.yml"
|
|
29
|
+
},
|
|
30
|
+
"client": {
|
|
31
|
+
"inject": [
|
|
32
|
+
"@deepseek-ai/dsh-client-connection",
|
|
33
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
34
|
+
"@deepseek-ai/dsh-client-locale",
|
|
35
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
36
|
+
"@deepseek-ai/dsh-client-ui-conversation",
|
|
37
|
+
"@deepseek-ai/dsh-client-ui-slots",
|
|
38
|
+
"@deepseek-ai/dsh-api-remotes"
|
|
39
|
+
],
|
|
40
|
+
"platform": "web",
|
|
41
|
+
"immediately": true
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"keywords": [
|
|
45
|
+
"deepseek",
|
|
46
|
+
"harness",
|
|
47
|
+
"dsh",
|
|
48
|
+
"dsh-plugin",
|
|
49
|
+
"skill",
|
|
50
|
+
"mcp",
|
|
51
|
+
"cordis"
|
|
52
|
+
],
|
|
53
|
+
"repository": {
|
|
54
|
+
"type": "git",
|
|
55
|
+
"url": "git+https://github.com/Max-Null/dsh-skill-mcp-center.git"
|
|
56
|
+
},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"build": "tsc -p tsconfig.json && node build-client.mjs",
|
|
59
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
60
|
+
},
|
|
61
|
+
"dependencies": {
|
|
62
|
+
"js-yaml": "^4.1.0"
|
|
63
|
+
},
|
|
64
|
+
"peerDependencies": {
|
|
65
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
66
|
+
"@deepseek-ai/cordis-plugin-loader": "^1.0.2",
|
|
67
|
+
"@deepseek-ai/dsh-client-connection": ">=0.0.1-rc.5",
|
|
68
|
+
"@deepseek-ai/dsh-host-apiproxy": ">=0.0.1-rc.5",
|
|
69
|
+
"@deepseek-ai/dsh-skill": "^0.1.0-rc.7",
|
|
70
|
+
"@deepseek-ai/dsh-tools": "^0.1.0-rc.7"
|
|
71
|
+
},
|
|
72
|
+
"devDependencies": {
|
|
73
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
74
|
+
"@deepseek-ai/cordis-plugin-loader": "^1.0.2",
|
|
75
|
+
"@deepseek-ai/dsh-client-connection": ">=0.0.1-rc.5",
|
|
76
|
+
"@deepseek-ai/dsh-host-apiproxy": ">=0.0.1-rc.5",
|
|
77
|
+
"@deepseek-ai/dsh-skill": "^0.1.0-rc.7",
|
|
78
|
+
"@deepseek-ai/dsh-tools": "^0.1.0-rc.7",
|
|
79
|
+
"@types/js-yaml": "^4.0.9",
|
|
80
|
+
"@types/node": "^26.2.0",
|
|
81
|
+
"@types/react": "~18.3.1",
|
|
82
|
+
"esbuild": "^0.24.0",
|
|
83
|
+
"react": "^18.2.0",
|
|
84
|
+
"typescript": "^5.5.0"
|
|
85
|
+
}
|
|
86
|
+
}
|