dsh-desktop-kit 2.7.6 → 2.7.8
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/CHANGELOG.md +14 -0
- package/README.md +19 -17
- package/client.js +352 -3
- package/docs/TEST-2.7.7.md +25 -0
- package/docs/TEST-2.7.8.md +29 -0
- package/package.json +2 -2
- package/plugins/dsh-guardrails/README.md +13 -0
- package/plugins/dsh-guardrails/client.js +220 -0
- package/plugins/dsh-guardrails/index.js +6 -0
- package/plugins/dsh-guardrails/lib/api.js +103 -0
- package/plugins/dsh-guardrails/package.json +2 -0
- package/plugins/dsh-guardrails/screenshots/permissions-1440.png +0 -0
- package/plugins/dsh-guardrails/screenshots/permissions-390.png +0 -0
- package/plugins/dsh-mcp-manager/README.md +13 -3
- package/plugins/dsh-mcp-manager/client.js +137 -1
- package/plugins/dsh-mcp-manager/index.js +27 -4
- package/plugins/dsh-mcp-manager/lib/api.js +33 -2
- package/plugins/dsh-mcp-manager/lib/client.js +18 -0
- package/plugins/dsh-mcp-manager/lib/manager.js +14 -2
- package/plugins/dsh-mcp-manager/screenshots/prompts-1440.png +0 -0
- package/plugins/dsh-mcp-manager/screenshots/prompts-390.png +0 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-guardrails — Permissions settings page (read-only).
|
|
3
|
+
*
|
|
4
|
+
* The DSH permission picker remains the only way to change the mode. This page
|
|
5
|
+
* explains the effective preset and shows the kit's independent hard rules.
|
|
6
|
+
*/
|
|
7
|
+
window.__ModuleLoader__.load({
|
|
8
|
+
id: "dsh-guardrails",
|
|
9
|
+
factory: (require) => {
|
|
10
|
+
var module = { exports: {} };
|
|
11
|
+
var exports = module.exports;
|
|
12
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
13
|
+
const React = require("react");
|
|
14
|
+
const h = React.createElement;
|
|
15
|
+
const useEffect = React.useEffect;
|
|
16
|
+
const useState = React.useState;
|
|
17
|
+
|
|
18
|
+
const CSS = `
|
|
19
|
+
.pr-root{--pr-line:var(--dsw-alias-border-l1,rgba(20,40,36,.13));--pr-soft:var(--dsw-alias-bg-layer-1,#f4f7f5);--pr-panel:var(--dsw-alias-bg-layer-2,#fff);--pr-text:var(--dsw-alias-label-primary,#17231f);--pr-muted:var(--dsw-alias-label-secondary,#61706b);--pr-accent:var(--dsw-alias-brand-primary,#0f766e);display:flex;flex-direction:column;gap:20px;max-width:1040px;min-width:0;color:var(--pr-text)}
|
|
20
|
+
.pr-head{display:flex;align-items:flex-end;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
|
21
|
+
.pr-eyebrow{margin:0 0 6px;font-size:11px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:var(--pr-accent)}
|
|
22
|
+
.pr-title{margin:0;font-size:clamp(24px,2.6vw,32px);line-height:1.1;letter-spacing:-.025em;font-weight:680}
|
|
23
|
+
.pr-sub{margin:8px 0 0;max-width:68ch;color:var(--pr-muted);font-size:14px;line-height:1.5}
|
|
24
|
+
.pr-mode{padding:20px;border:1px solid var(--pr-line);border-radius:18px;background:linear-gradient(145deg,var(--pr-panel),var(--pr-soft));min-width:0}
|
|
25
|
+
.pr-mode-top{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;flex-wrap:wrap}
|
|
26
|
+
.pr-mode h3{margin:0;font-size:24px;line-height:1.1;letter-spacing:-.03em}
|
|
27
|
+
.pr-chips{display:flex;gap:6px;flex-wrap:wrap}
|
|
28
|
+
.pr-chip{padding:3px 8px;border:1px solid var(--pr-line);border-radius:999px;background:var(--pr-panel);font-size:10px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--pr-muted);white-space:nowrap}
|
|
29
|
+
.pr-chip.sandbox{color:#0f766e;border-color:rgba(15,118,110,.25);background:rgba(15,118,110,.07)}
|
|
30
|
+
.pr-chip.approval{color:#9a3412;border-color:rgba(154,52,18,.22);background:rgba(154,52,18,.06)}
|
|
31
|
+
.pr-mode p{margin:8px 0 0;color:var(--pr-muted);font-size:13px;line-height:1.5;max-width:70ch}
|
|
32
|
+
.pr-note{display:flex;gap:8px;align-items:flex-start;margin-top:14px;padding:10px 12px;border:1px solid rgba(15,118,110,.16);border-radius:10px;background:rgba(15,118,110,.05);color:var(--pr-muted);font-size:12px;line-height:1.45}
|
|
33
|
+
.pr-note b{color:var(--pr-text);font-weight:650}
|
|
34
|
+
.pr-stats{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}
|
|
35
|
+
.pr-stat{padding:14px 16px;border:1px solid var(--pr-line);border-radius:14px;background:var(--pr-panel);min-width:0}
|
|
36
|
+
.pr-stat b{display:block;font-size:24px;line-height:1.1;letter-spacing:-.03em;font-variant-numeric:tabular-nums}
|
|
37
|
+
.pr-stat span{display:block;margin-top:5px;color:var(--pr-muted);font-size:11px;font-weight:650;letter-spacing:.07em;text-transform:uppercase}
|
|
38
|
+
.pr-section{display:flex;flex-direction:column;gap:10px;min-width:0}
|
|
39
|
+
.pr-section h3{margin:0;font-size:15px;letter-spacing:-.01em}
|
|
40
|
+
.pr-section .pr-hint{margin:0;color:var(--pr-muted);font-size:12px;line-height:1.45}
|
|
41
|
+
.pr-presets{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}
|
|
42
|
+
.pr-preset{padding:12px;border:1px solid var(--pr-line);border-radius:12px;background:var(--pr-panel);min-width:0}
|
|
43
|
+
.pr-preset.active{border-color:rgba(15,118,110,.35);background:rgba(15,118,110,.06)}
|
|
44
|
+
.pr-preset b{display:flex;justify-content:space-between;gap:8px;font-size:13px;align-items:center}
|
|
45
|
+
.pr-preset span{display:block;margin-top:4px;color:var(--pr-muted);font-size:11.5px;line-height:1.4}
|
|
46
|
+
.pr-preset em{font-style:normal;color:var(--pr-accent);font-size:10px;font-weight:700;letter-spacing:.08em;text-transform:uppercase}
|
|
47
|
+
.pr-list{display:flex;flex-direction:column;border:1px solid var(--pr-line);border-radius:14px;overflow:hidden;background:var(--pr-panel)}
|
|
48
|
+
.pr-row{display:flex;gap:10px;align-items:baseline;padding:9px 12px;border-top:1px solid var(--pr-line);font-size:12px;line-height:1.4;min-width:0}
|
|
49
|
+
.pr-row:first-child{border-top:0}
|
|
50
|
+
.pr-sev{flex:none;width:46px;font-size:9.5px;font-weight:750;letter-spacing:.08em;text-transform:uppercase}
|
|
51
|
+
.pr-sev.deny{color:#9f1239}.pr-sev.ask{color:#9a3412}
|
|
52
|
+
.pr-row code{flex:none;color:var(--pr-text);font:inherit;font-weight:650;white-space:nowrap}
|
|
53
|
+
.pr-row span{color:var(--pr-muted);overflow:hidden;text-overflow:ellipsis;min-width:0}
|
|
54
|
+
.pr-path{padding:9px 12px;border-top:1px solid var(--pr-line);font-size:12px;line-height:1.45;min-width:0}
|
|
55
|
+
.pr-path:first-child{border-top:0}
|
|
56
|
+
.pr-path b{display:block;font-size:12px}
|
|
57
|
+
.pr-path code{display:block;margin-top:3px;color:var(--pr-muted);font:11.5px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace;overflow-wrap:anywhere}
|
|
58
|
+
.pr-error{padding:8px 10px;border:1px solid rgba(159,18,57,.16);border-radius:9px;background:rgba(159,18,57,.06);color:#9f1239;font-size:12px;line-height:1.4}
|
|
59
|
+
.pr-loading{color:var(--pr-muted);font-size:13px;padding:18px 0}
|
|
60
|
+
.pr-empty{padding:18px;border:1px dashed var(--pr-line);border-radius:14px;color:var(--pr-muted);font-size:12.5px;line-height:1.5;background:linear-gradient(160deg,var(--pr-soft),transparent 75%)}
|
|
61
|
+
.pr-foot{display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap;color:var(--pr-muted);font-size:11px;line-height:1.4}
|
|
62
|
+
@media(max-width:860px){.pr-presets{grid-template-columns:1fr 1fr}.pr-stats{grid-template-columns:1fr 1fr}}
|
|
63
|
+
@media(max-width:520px){.pr-presets{grid-template-columns:1fr}.pr-stats{grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}.pr-stat{padding:10px 12px}.pr-stat b{font-size:20px}.pr-stat span{font-size:9px;letter-spacing:.04em}.pr-root{gap:16px}.pr-mode{padding:16px}.pr-note{flex-direction:column;gap:4px}.pr-row{display:flex;flex-wrap:wrap;gap:4px 8px}.pr-row code{white-space:normal;overflow-wrap:anywhere}.pr-row span{flex-basis:100%;padding-left:0}}
|
|
64
|
+
`;
|
|
65
|
+
|
|
66
|
+
const SEVERITY_ORDER = { deny: 0, ask: 1 };
|
|
67
|
+
|
|
68
|
+
function currentSessionSelector(state) {
|
|
69
|
+
try {
|
|
70
|
+
const sessions = Object.values(state.byId || {});
|
|
71
|
+
const main = sessions.find((session) => session && session.retainedBy && (session.retainedBy.mainView || 0) > 0);
|
|
72
|
+
if (main) return main.id;
|
|
73
|
+
const ids = Array.isArray(state.ids) ? state.ids : [];
|
|
74
|
+
return ids.length > 0 ? ids[ids.length - 1] : undefined;
|
|
75
|
+
} catch { return undefined; }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function call(ctx, operation, payload) {
|
|
79
|
+
let signal;
|
|
80
|
+
try { signal = AbortSignal.timeout(30000); } catch { signal = undefined; }
|
|
81
|
+
const result = await ctx.connection.rpc.call("/api", `dsh-guardrails/${operation}`, payload ?? {}, signal);
|
|
82
|
+
if (!result || result.ok !== true) throw new Error(result?.error?.message || "Ошибка RPC");
|
|
83
|
+
return result.value;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function errorOf(value) { return value instanceof Error ? value.message : String(value); }
|
|
87
|
+
|
|
88
|
+
function SandboxChip({ value }) {
|
|
89
|
+
const labels = {
|
|
90
|
+
"read-only": "Read only",
|
|
91
|
+
"workspace-write": "Workspace write",
|
|
92
|
+
"danger-full-access": "Full access",
|
|
93
|
+
};
|
|
94
|
+
return h("span", { className: "pr-chip sandbox" }, labels[value] || value || "sandbox");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function PermissionsSection(props) {
|
|
98
|
+
const [state, setState] = useState({ loading: true, error: null, data: null });
|
|
99
|
+
const useSessions = props && props.useSessions;
|
|
100
|
+
const useWorkspaces = props && props.useWorkspaces;
|
|
101
|
+
const sessionId = useSessions ? useSessions(currentSessionSelector) : undefined;
|
|
102
|
+
const workspaceSnapshot = useWorkspaces ? useWorkspaces((snapshot) => snapshot) : undefined;
|
|
103
|
+
const workspaces = Array.isArray(workspaceSnapshot?.items) ? workspaceSnapshot.items : [];
|
|
104
|
+
const workspace = workspaces.find((item) => Array.isArray(item.sessionIds) && item.sessionIds.includes(sessionId))
|
|
105
|
+
?? workspaces.find((item) => Array.isArray(item.sessionIds) && item.sessionIds.length > 0)
|
|
106
|
+
?? workspaces[0];
|
|
107
|
+
const cwd = typeof workspace?.path === "string" ? workspace.path : undefined;
|
|
108
|
+
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
let cancelled = false;
|
|
111
|
+
(async () => {
|
|
112
|
+
try {
|
|
113
|
+
const data = await call(window.__DSH_GUARDRAILS_CTX__, "permissions", { sessionId, cwd });
|
|
114
|
+
if (!cancelled) setState({ loading: false, error: null, data });
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (!cancelled) setState({ loading: false, error: errorOf(error), data: null });
|
|
117
|
+
}
|
|
118
|
+
})();
|
|
119
|
+
return () => { cancelled = true; };
|
|
120
|
+
}, [sessionId, cwd]);
|
|
121
|
+
|
|
122
|
+
const preset = state.data?.preset;
|
|
123
|
+
const guard = state.data?.guard;
|
|
124
|
+
const options = Array.isArray(preset?.catalog?.options) ? preset.catalog.options : [];
|
|
125
|
+
const rules = Array.isArray(guard?.rules)
|
|
126
|
+
? [...guard.rules].sort((a, b) => (SEVERITY_ORDER[a.severity] ?? 9) - (SEVERITY_ORDER[b.severity] ?? 9))
|
|
127
|
+
: [];
|
|
128
|
+
const protectedPaths = Array.isArray(guard?.protectedPaths) ? guard.protectedPaths : [];
|
|
129
|
+
const current = preset?.current || "custom";
|
|
130
|
+
const spec = preset?.spec;
|
|
131
|
+
const option = preset?.option;
|
|
132
|
+
const title = option?.name || current;
|
|
133
|
+
|
|
134
|
+
return h("section", { className: "pr-root", "aria-label": "Права и ограничения" },
|
|
135
|
+
h("div", { className: "pr-head" },
|
|
136
|
+
h("div", null,
|
|
137
|
+
h("p", { className: "pr-eyebrow" }, "Guardrails · effective permissions"),
|
|
138
|
+
h("h2", { className: "pr-title" }, "Права"),
|
|
139
|
+
h("p", { className: "pr-sub" }, "Что разрешено в этом чате и что kit запрещает в любом режиме. Режим переключается штатным picker DSH в композере — здесь только прозрачный срез."),
|
|
140
|
+
),
|
|
141
|
+
),
|
|
142
|
+
state.error ? h("div", { className: "pr-error", role: "alert" }, state.error) : null,
|
|
143
|
+
state.loading ? h("div", { className: "pr-loading" }, "Читаем эффективные права…") : null,
|
|
144
|
+
!state.loading && state.data ? h(React.Fragment, null,
|
|
145
|
+
h("div", { className: "pr-mode" },
|
|
146
|
+
h("div", { className: "pr-mode-top" },
|
|
147
|
+
h("div", null,
|
|
148
|
+
h("h3", null, title),
|
|
149
|
+
h("p", null, option?.description || spec?.description || "Эффективный режим этого чата."),
|
|
150
|
+
),
|
|
151
|
+
h("div", { className: "pr-chips" },
|
|
152
|
+
h(SandboxChip, { value: spec?.sandbox }),
|
|
153
|
+
h("span", { className: "pr-chip approval" }, spec?.approval === "never" ? "approval never" : "approval ask"),
|
|
154
|
+
),
|
|
155
|
+
),
|
|
156
|
+
h("div", { className: "pr-note" },
|
|
157
|
+
h("b", null, "Не второй picker."),
|
|
158
|
+
h("span", null, "Смена режима — только в DSH. Эта страница не меняет разрешения и не хранит approvals; каждая карточка подтверждения остаётся в чате."),
|
|
159
|
+
),
|
|
160
|
+
),
|
|
161
|
+
h("div", { className: "pr-stats" },
|
|
162
|
+
h("div", { className: "pr-stat" }, h("b", null, rules.length), h("span", null, "жёстких правил")),
|
|
163
|
+
h("div", { className: "pr-stat" }, h("b", null, protectedPaths.length), h("span", null, "защищённых путей")),
|
|
164
|
+
h("div", { className: "pr-stat" }, h("b", null, options.length), h("span", null, "пресетов DSH")),
|
|
165
|
+
),
|
|
166
|
+
h("div", { className: "pr-section" },
|
|
167
|
+
h("h3", null, "Пресеты DSH"),
|
|
168
|
+
h("p", { className: "pr-hint" }, "Доступны в композере. Текущий выделен; kit не дублирует переключатель."),
|
|
169
|
+
h("div", { className: "pr-presets" }, options.map((item) => h("div", { key: item.value, className: `pr-preset${item.value === current ? " active" : ""}` },
|
|
170
|
+
h("b", null, h("span", null, item.name || item.value), item.value === current ? h("em", null, "сейчас") : null),
|
|
171
|
+
h("span", null, item.description || "Без описания"),
|
|
172
|
+
))),
|
|
173
|
+
),
|
|
174
|
+
h("div", { className: "pr-section" },
|
|
175
|
+
h("h3", null, "Жёсткие правила kit"),
|
|
176
|
+
h("p", { className: "pr-hint" }, "Действуют даже в Full access. Отключаются только политикой guardrails, не режимом DSH."),
|
|
177
|
+
rules.length > 0 ? h("div", { className: "pr-list" }, rules.map((rule) => h("div", { className: "pr-row", key: rule.id },
|
|
178
|
+
h("span", { className: `pr-sev ${rule.severity}` }, rule.severity),
|
|
179
|
+
h("code", null, rule.id),
|
|
180
|
+
h("span", { title: rule.description }, rule.description || ""),
|
|
181
|
+
))) : h("div", { className: "pr-empty" }, "Правила отключены политикой guardrails."),
|
|
182
|
+
),
|
|
183
|
+
h("div", { className: "pr-section" },
|
|
184
|
+
h("h3", null, "Защищённые пути"),
|
|
185
|
+
h("p", { className: "pr-hint" }, "Запись, редактирование и удаление этих путей отклоняются до выполнения."),
|
|
186
|
+
protectedPaths.length > 0 ? h("div", { className: "pr-list" }, protectedPaths.map((entry) => h("div", { className: "pr-path", key: entry.id },
|
|
187
|
+
h("b", null, entry.id),
|
|
188
|
+
h("code", null, `${entry.match} /${entry.pattern}/`),
|
|
189
|
+
))) : h("div", { className: "pr-empty" }, "Защищённые пути отключены."),
|
|
190
|
+
),
|
|
191
|
+
) : null,
|
|
192
|
+
h("div", { className: "pr-foot" },
|
|
193
|
+
h("span", null, guard?.policyPath ? `Политика: ${guard.policyPath}` : "Политика guardrails"),
|
|
194
|
+
h("span", null, guard ? (guard.enabled ? "hooks активны" : "hooks отключены") : ""),
|
|
195
|
+
),
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function apply(ctx) {
|
|
200
|
+
window.__DSH_GUARDRAILS_CTX__ = ctx;
|
|
201
|
+
ctx.effect(() => {
|
|
202
|
+
const style = document.createElement("style");
|
|
203
|
+
style.id = "dsh-guardrails";
|
|
204
|
+
style.textContent = CSS;
|
|
205
|
+
document.head.appendChild(style);
|
|
206
|
+
return () => style.remove();
|
|
207
|
+
}, "guardrails: styles");
|
|
208
|
+
ctx.effect(() => ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
209
|
+
name: "settings.section",
|
|
210
|
+
id: "guardrails",
|
|
211
|
+
order: 35,
|
|
212
|
+
label: () => "Права",
|
|
213
|
+
}, PermissionsSection)), "guardrails: settings");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
exports.apply = apply;
|
|
217
|
+
exports.inject = ["slots", "connection"];
|
|
218
|
+
return module.exports;
|
|
219
|
+
},
|
|
220
|
+
});
|
|
@@ -21,6 +21,7 @@ import fs from "node:fs";
|
|
|
21
21
|
import path from "node:path";
|
|
22
22
|
|
|
23
23
|
import { createGuard, createHookState, registerHooks } from "./lib/hooks.js";
|
|
24
|
+
import { registerPermissionsApi } from "./lib/api.js";
|
|
24
25
|
import { resolveMode } from "./lib/modes.js";
|
|
25
26
|
import {
|
|
26
27
|
DEFAULT_COMMAND_RULES,
|
|
@@ -385,5 +386,10 @@ export function apply(ctx) {
|
|
|
385
386
|
};
|
|
386
387
|
|
|
387
388
|
ctx.tools.register(tool);
|
|
389
|
+
try {
|
|
390
|
+
ctx.inject(["connection"], (child) => registerPermissionsApi(child));
|
|
391
|
+
} catch (error) {
|
|
392
|
+
logger?.warn?.(`[guardrails] permissions UI skipped: ${messageOf(error)}`);
|
|
393
|
+
}
|
|
388
394
|
logger?.info?.(`[guardrails] tool ready (policy: ${policyPath(env)})`);
|
|
389
395
|
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only Permissions RPC.
|
|
3
|
+
*
|
|
4
|
+
* DSH owns the permission picker and the approval card. This endpoint only
|
|
5
|
+
* reports the effective preset plus the kit's independent hard rules, so the
|
|
6
|
+
* Settings page never becomes a second source of truth.
|
|
7
|
+
*/
|
|
8
|
+
import { loadPolicy } from "./policy.js";
|
|
9
|
+
|
|
10
|
+
export const RPC_NAMESPACE = "dsh-guardrails";
|
|
11
|
+
const ROUTE_BASE = "/api/dsh-guardrails";
|
|
12
|
+
|
|
13
|
+
function ok(rpcId, value) {
|
|
14
|
+
return Response.json({ type: "server-response", rpcId, result: { ok: true, value } });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function fail(rpcId, message) {
|
|
18
|
+
return Response.json({ type: "server-response", rpcId, result: { ok: false, error: { code: "guardrails/bad-request", message, details: {} } } });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function envelope(request) {
|
|
22
|
+
const body = await request.json();
|
|
23
|
+
if (!body || body.type !== "client-request" || typeof body.rpcId !== "string" || typeof body.method !== "string") {
|
|
24
|
+
throw new Error("bad Connection envelope");
|
|
25
|
+
}
|
|
26
|
+
return { rpcId: body.rpcId, method: body.method, payload: body.payload ?? {} };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function liveSession(ctx, payload) {
|
|
30
|
+
const sessions = ctx.get?.("sessions");
|
|
31
|
+
if (payload?.sessionId) {
|
|
32
|
+
const direct = sessions?.get?.(payload.sessionId);
|
|
33
|
+
if (direct) return direct;
|
|
34
|
+
}
|
|
35
|
+
const live = typeof sessions?.list === "function" ? sessions.list() : [];
|
|
36
|
+
return live.length > 0 ? live[live.length - 1] : undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function presetSnapshot(ctx, session) {
|
|
40
|
+
const service = ctx.get?.("permissionPresets");
|
|
41
|
+
if (!service) return { available: false, current: null, spec: null, option: null, catalog: { options: [], defaultPreset: null } };
|
|
42
|
+
let catalog = { options: [], defaultPreset: null };
|
|
43
|
+
try { catalog = service.catalog(); } catch { /* optional service */ }
|
|
44
|
+
let name = null;
|
|
45
|
+
try { name = session ? service.current(session) : null; } catch { name = null; }
|
|
46
|
+
let spec = null;
|
|
47
|
+
let option = null;
|
|
48
|
+
if (name) {
|
|
49
|
+
try { spec = service.resolve(name); } catch { spec = null; }
|
|
50
|
+
try { option = service.optionOf(name); } catch { option = null; }
|
|
51
|
+
}
|
|
52
|
+
return { available: true, current: name, spec, option, catalog };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function guardSnapshot(env) {
|
|
56
|
+
const loaded = loadPolicy({ env });
|
|
57
|
+
const rules = loaded.policy.rules
|
|
58
|
+
.filter((rule) => rule.enabled !== false)
|
|
59
|
+
.map((rule) => ({ id: rule.id, severity: rule.severity, description: rule.description }));
|
|
60
|
+
const protectedPaths = loaded.policy.protectedPaths
|
|
61
|
+
.filter((entry) => entry.enabled !== false)
|
|
62
|
+
.map((entry) => ({ id: entry.id, match: entry.match, pattern: entry.pattern, description: entry.description }));
|
|
63
|
+
return {
|
|
64
|
+
enabled: loaded.enabled,
|
|
65
|
+
killSwitch: loaded.killSwitch,
|
|
66
|
+
policyPath: loaded.path,
|
|
67
|
+
loadedFromFile: loaded.loadedFromFile,
|
|
68
|
+
error: loaded.error,
|
|
69
|
+
warnings: loaded.warnings,
|
|
70
|
+
rules,
|
|
71
|
+
protectedPaths,
|
|
72
|
+
hooks: loaded.policy.hooks,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function registerPermissionsApi(ctx) {
|
|
77
|
+
const route = {
|
|
78
|
+
path: `${ROUTE_BASE}/permissions`,
|
|
79
|
+
methods: ["POST"],
|
|
80
|
+
requestBody: "buffered",
|
|
81
|
+
fetch: async (request) => {
|
|
82
|
+
let rpcId = "invalid-request";
|
|
83
|
+
try {
|
|
84
|
+
const value = await envelope(request);
|
|
85
|
+
rpcId = value.rpcId;
|
|
86
|
+
if (value.method !== `${RPC_NAMESPACE}/permissions`) throw new Error("endpoint/method mismatch");
|
|
87
|
+
const session = liveSession(ctx, value.payload);
|
|
88
|
+
const preset = presetSnapshot(ctx, session);
|
|
89
|
+
return ok(rpcId, {
|
|
90
|
+
project: session?.header?.cwd ?? null,
|
|
91
|
+
preset,
|
|
92
|
+
guard: guardSnapshot(process.env),
|
|
93
|
+
});
|
|
94
|
+
} catch (error) {
|
|
95
|
+
return fail(rpcId, error instanceof Error ? error.message : String(error));
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
ctx.effect(() => {
|
|
100
|
+
const dispose = ctx.connection.fetch.register(route);
|
|
101
|
+
return () => { try { void Promise.resolve(dispose()).catch(() => {}); } catch { /* unloading */ } };
|
|
102
|
+
}, "guardrails: permissions route");
|
|
103
|
+
}
|
|
@@ -7,11 +7,13 @@
|
|
|
7
7
|
"main": "index.js",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": "./index.js",
|
|
10
|
+
"./client": "./client.js",
|
|
10
11
|
"./lib": "./lib/scan.js",
|
|
11
12
|
"./package.json": "./package.json"
|
|
12
13
|
},
|
|
13
14
|
"files": [
|
|
14
15
|
"index.js",
|
|
16
|
+
"client.js",
|
|
15
17
|
"lib/**",
|
|
16
18
|
"tools/**",
|
|
17
19
|
"cordis.patch.yml",
|
|
Binary file
|
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# dsh-mcp-manager
|
|
2
2
|
|
|
3
|
-
MCP-серверы для DSH без SDK-зависимости: stdio и streamable HTTP, tools, resources, trust-гейт и премиальный экран в Settings.
|
|
3
|
+
MCP-серверы для DSH без SDK-зависимости: stdio и streamable HTTP, tools, resources, prompts, trust-гейт и премиальный экран в Settings.
|
|
4
4
|
|
|
5
5
|

|
|
6
6
|
|
|
@@ -12,6 +12,7 @@ MCP-серверы для DSH без SDK-зависимости: stdio и stream
|
|
|
12
12
|
- переключатели «Включён» и «Доверен»;
|
|
13
13
|
- «Проверить» — переподключение и health;
|
|
14
14
|
- «Tools» — список инструментов с описаниями;
|
|
15
|
+
- «Prompts» — промпты сервера: аргументы, запуск и копирование результата;
|
|
15
16
|
- «Добавить сервер» — пресеты GitHub / PostgreSQL / Filesystem или ручная форма;
|
|
16
17
|
- токен сохраняется в `~/.dsh/mcp.secrets.json` (0600), а в `mcp.json` остаётся только `${secret:…}`;
|
|
17
18
|
- новые tools появляются в **новом чате** после подключения.
|
|
@@ -20,6 +21,14 @@ MCP-серверы для DSH без SDK-зависимости: stdio и stream
|
|
|
20
21
|
|
|
21
22
|
<p><img src="screenshots/settings-390.png" alt="MCP Settings на 390px" width="390"> <img src="screenshots/modal-390.png" alt="Форма добавления MCP-сервера" width="390"></p>
|
|
22
23
|
|
|
24
|
+
### Prompts
|
|
25
|
+
|
|
26
|
+
Промпт — это готовый запрос с аргументами, который отдаёт сам MCP-сервер. Нажмите **Prompts**, заполните поля и получите текст. Результат можно скопировать одной кнопкой.
|
|
27
|
+
|
|
28
|
+

|
|
29
|
+
|
|
30
|
+
<img src="screenshots/prompts-390.png" alt="MCP Prompts на 390px" width="390">
|
|
31
|
+
|
|
23
32
|
## Конфиг
|
|
24
33
|
|
|
25
34
|
`~/.dsh/mcp.json` — глобальные серверы, стартуют автоматически при `trusted: true`.
|
|
@@ -61,6 +70,8 @@ MCP-серверы для DSH без SDK-зависимости: stdio и stream
|
|
|
61
70
|
/mcp init
|
|
62
71
|
/mcp list
|
|
63
72
|
/mcp tools github
|
|
73
|
+
/mcp prompts github
|
|
74
|
+
/mcp prompt github review {"target":"src/app.js"}
|
|
64
75
|
/mcp call github search_repositories {"query":"dsh-desktop-kit"}
|
|
65
76
|
/mcp resources postgres
|
|
66
77
|
/mcp read postgres postgres://schema
|
|
@@ -72,7 +83,6 @@ MCP-серверы для DSH без SDK-зависимости: stdio и stream
|
|
|
72
83
|
## Ограничения
|
|
73
84
|
|
|
74
85
|
- Нет OAuth: только env/headers. Токены кладите в переменные окружения.
|
|
75
|
-
- Нет
|
|
76
|
-
- Нет prompts и sampling; поддержаны tools/resources.
|
|
86
|
+
- Нет sampling: сервер не может запросить генерацию у модели.
|
|
77
87
|
- HTTP-транспорт поддерживает JSON и простой SSE-ответ; интерактивный SSE-стрим с серверными запросами не реализован.
|
|
78
88
|
- MCP-сервер — произвольный процесс. Доверяйте только своим конфигам.
|
|
@@ -64,6 +64,12 @@ window.__ModuleLoader__.load({
|
|
|
64
64
|
.mcp-tool{display:flex;gap:8px;align-items:baseline;font-size:12px;line-height:1.45;min-width:0}
|
|
65
65
|
.mcp-tool code{color:var(--mcp-accent);font:inherit;font-weight:650;white-space:nowrap}
|
|
66
66
|
.mcp-tool span{color:var(--mcp-muted);overflow:hidden;text-overflow:ellipsis}
|
|
67
|
+
.mcp-prompt-list{display:flex;flex-direction:column;gap:8px}
|
|
68
|
+
.mcp-prompt-card{text-align:left;border:1px solid var(--mcp-line);background:var(--mcp-panel);border-radius:12px;padding:11px 12px;cursor:pointer;font:inherit;color:inherit;min-height:44px}
|
|
69
|
+
.mcp-prompt-card:hover{border-color:rgba(15,118,110,.4);background:var(--mcp-accent-soft)}
|
|
70
|
+
.mcp-prompt-card b{display:block;font-size:13px}
|
|
71
|
+
.mcp-prompt-card span{display:block;color:var(--mcp-muted);font-size:12px;line-height:1.4;margin-top:3px}
|
|
72
|
+
.mcp-prompt-output{margin:0;padding:12px;border:1px solid var(--mcp-line);border-radius:10px;background:var(--mcp-soft);color:var(--mcp-text);font:12px/1.55 ui-monospace,SFMono-Regular,Consolas,monospace;white-space:pre-wrap;overflow-wrap:anywhere;max-height:38vh;overflow:auto}
|
|
67
73
|
.mcp-empty{border:1px dashed var(--mcp-line);border-radius:18px;padding:26px;background:linear-gradient(160deg,var(--mcp-soft),transparent 75%);display:flex;flex-direction:column;gap:12px;align-items:flex-start}
|
|
68
74
|
.mcp-empty h3{margin:0;font-size:18px;letter-spacing:-.015em}
|
|
69
75
|
.mcp-empty p{margin:0;color:var(--mcp-muted);font-size:13px;line-height:1.5;max-width:60ch}
|
|
@@ -118,7 +124,7 @@ window.__ModuleLoader__.load({
|
|
|
118
124
|
);
|
|
119
125
|
}
|
|
120
126
|
|
|
121
|
-
function ServerCard({ server, busy, onToggle, onTest, onTools, onRemove, tools }) {
|
|
127
|
+
function ServerCard({ server, busy, onToggle, onTest, onTools, onPrompts, onRemove, tools }) {
|
|
122
128
|
const [open, setOpen] = useState(false);
|
|
123
129
|
const connected = server.connected;
|
|
124
130
|
const status = connected ? "on" : (server.error ? "err" : "off");
|
|
@@ -141,6 +147,7 @@ window.__ModuleLoader__.load({
|
|
|
141
147
|
h(Toggle, { checked: server.trusted, label: "Доверен", disabled: busy, onChange: (value) => onToggle(server.id, { trusted: value }) }),
|
|
142
148
|
h("div", { className: "mcp-actions" },
|
|
143
149
|
h("button", { className: "mcp-btn ghost", disabled: busy, onClick: () => onTest(server.id) }, "Проверить"),
|
|
150
|
+
h("button", { className: "mcp-btn ghost", disabled: busy, onClick: () => onPrompts(server.id) }, "Prompts"),
|
|
144
151
|
h("button", { className: "mcp-btn ghost", disabled: busy, onClick: () => { setOpen(!open); if (!open) onTools(server.id); } }, open ? "Скрыть" : "Tools"),
|
|
145
152
|
h("button", { className: "mcp-btn danger ghost", disabled: busy, onClick: () => onRemove(server.id) }, "Удалить"),
|
|
146
153
|
),
|
|
@@ -156,6 +163,58 @@ window.__ModuleLoader__.load({
|
|
|
156
163
|
);
|
|
157
164
|
}
|
|
158
165
|
|
|
166
|
+
function PromptModal({ state, onClose, onPick, onBack, onArg, onRun, onCopy }) {
|
|
167
|
+
if (!state) return null;
|
|
168
|
+
const prompt = state.prompt;
|
|
169
|
+
const args = Array.isArray(prompt?.arguments) ? prompt.arguments : [];
|
|
170
|
+
return h("div", { className: "mcp-overlay", onClick: onClose },
|
|
171
|
+
h("div", {
|
|
172
|
+
className: "mcp-modal",
|
|
173
|
+
role: "dialog",
|
|
174
|
+
"aria-modal": "true",
|
|
175
|
+
"aria-labelledby": "mcp-prompts-title",
|
|
176
|
+
onClick: (event) => event.stopPropagation(),
|
|
177
|
+
},
|
|
178
|
+
h("div", { className: "mcp-modal-head" },
|
|
179
|
+
h("div", null,
|
|
180
|
+
h("h2", { id: "mcp-prompts-title" }, prompt ? (prompt.title || prompt.name) : "Prompts"),
|
|
181
|
+
h("p", null, prompt ? (prompt.description || "Промпт MCP-сервера") : `Промпты сервера «${state.serverId}».`),
|
|
182
|
+
),
|
|
183
|
+
h("button", { className: "mcp-btn ghost", type: "button", onClick: onClose, "aria-label": "Закрыть" }, "Закрыть"),
|
|
184
|
+
),
|
|
185
|
+
state.error ? h("div", { className: "mcp-error", role: "alert" }, state.error) : null,
|
|
186
|
+
!prompt ? h("div", { className: "mcp-prompt-list" },
|
|
187
|
+
state.prompts.length === 0 ? h("div", { className: "mcp-meta" }, "У сервера нет prompts.") : state.prompts.map((item) => h("button", {
|
|
188
|
+
key: item.name,
|
|
189
|
+
type: "button",
|
|
190
|
+
className: "mcp-prompt-card",
|
|
191
|
+
onClick: () => onPick(item),
|
|
192
|
+
}, h("b", null, item.name), h("span", null, item.description || "Без описания"))),
|
|
193
|
+
) : h("div", { className: "mcp-form" },
|
|
194
|
+
args.map((arg, index) => h("div", { className: `mcp-field${String(arg.description || "").length > 40 ? " wide" : ""}`, key: arg.name },
|
|
195
|
+
h("label", { htmlFor: `mcp-prompt-arg-${index}` }, arg.name, arg.required ? " *" : ""),
|
|
196
|
+
h("input", {
|
|
197
|
+
id: `mcp-prompt-arg-${index}`,
|
|
198
|
+
value: state.args[arg.name] ?? "",
|
|
199
|
+
placeholder: arg.description || "",
|
|
200
|
+
onChange: (event) => onArg(arg.name, event.target.value),
|
|
201
|
+
}),
|
|
202
|
+
)),
|
|
203
|
+
args.length === 0 ? h("p", { className: "mcp-note", style: { gridColumn: "1/-1" } }, "У промпта нет аргументов.") : null,
|
|
204
|
+
state.result ? h("div", { className: "mcp-field wide" },
|
|
205
|
+
h("label", null, "Результат"),
|
|
206
|
+
h("pre", { className: "mcp-prompt-output" }, state.result),
|
|
207
|
+
) : null,
|
|
208
|
+
h("div", { className: "mcp-form-actions", style: { gridColumn: "1/-1" } },
|
|
209
|
+
state.prompts.length > 1 ? h("button", { className: "mcp-btn ghost", type: "button", onClick: onBack }, "Назад") : null,
|
|
210
|
+
state.result ? h("button", { className: "mcp-btn ghost", type: "button", onClick: onCopy }, "Копировать") : null,
|
|
211
|
+
h("button", { className: "mcp-btn primary", type: "button", disabled: state.busy, onClick: onRun }, state.busy ? "Запрашиваем…" : "Получить"),
|
|
212
|
+
),
|
|
213
|
+
),
|
|
214
|
+
),
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
159
218
|
function ServerForm({ initial, busy, onCancel, onSave }) {
|
|
160
219
|
const [form, setForm] = useState(initial);
|
|
161
220
|
const set = (patch) => setForm((current) => ({ ...current, ...patch }));
|
|
@@ -235,6 +294,7 @@ window.__ModuleLoader__.load({
|
|
|
235
294
|
const [form, setForm] = useState(null);
|
|
236
295
|
const [tools, setTools] = useState({});
|
|
237
296
|
const [toast, setToast] = useState("");
|
|
297
|
+
const [promptModal, setPromptModal] = useState(null);
|
|
238
298
|
|
|
239
299
|
const load = async () => {
|
|
240
300
|
try {
|
|
@@ -262,6 +322,72 @@ window.__ModuleLoader__.load({
|
|
|
262
322
|
}
|
|
263
323
|
};
|
|
264
324
|
|
|
325
|
+
const openPrompts = async (id) => {
|
|
326
|
+
setBusy(true);
|
|
327
|
+
setError(null);
|
|
328
|
+
try {
|
|
329
|
+
const result = await call(window.__DSH_MCP_CTX__, "prompts", { id });
|
|
330
|
+
setPromptModal({
|
|
331
|
+
serverId: id,
|
|
332
|
+
prompts: Array.isArray(result.prompts) ? result.prompts : [],
|
|
333
|
+
prompt: null,
|
|
334
|
+
args: {},
|
|
335
|
+
result: "",
|
|
336
|
+
busy: false,
|
|
337
|
+
error: result.error || "",
|
|
338
|
+
});
|
|
339
|
+
} catch (cause) {
|
|
340
|
+
setError(cause instanceof Error ? cause.message : String(cause));
|
|
341
|
+
} finally {
|
|
342
|
+
setBusy(false);
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const pickPrompt = (prompt) => {
|
|
347
|
+
setPromptModal((current) => current ? {
|
|
348
|
+
...current,
|
|
349
|
+
prompt,
|
|
350
|
+
args: Object.fromEntries((prompt.arguments ?? []).map((arg) => [arg.name, ""])),
|
|
351
|
+
result: "",
|
|
352
|
+
error: "",
|
|
353
|
+
} : current);
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
const runPrompt = async () => {
|
|
357
|
+
const current = promptModal;
|
|
358
|
+
if (!current?.prompt) return;
|
|
359
|
+
setPromptModal({ ...current, busy: true, error: "", result: "" });
|
|
360
|
+
try {
|
|
361
|
+
const result = await call(window.__DSH_MCP_CTX__, "prompt", {
|
|
362
|
+
id: current.serverId,
|
|
363
|
+
name: current.prompt.name,
|
|
364
|
+
arguments: current.args,
|
|
365
|
+
});
|
|
366
|
+
setPromptModal((latest) => latest ? {
|
|
367
|
+
...latest,
|
|
368
|
+
busy: false,
|
|
369
|
+
result: result.text || "",
|
|
370
|
+
error: result.text ? "" : "Сервер вернул пустой промпт",
|
|
371
|
+
} : latest);
|
|
372
|
+
} catch (cause) {
|
|
373
|
+
setPromptModal((latest) => latest ? {
|
|
374
|
+
...latest,
|
|
375
|
+
busy: false,
|
|
376
|
+
error: cause instanceof Error ? cause.message : String(cause),
|
|
377
|
+
} : latest);
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const copyPrompt = async () => {
|
|
382
|
+
try {
|
|
383
|
+
await navigator.clipboard.writeText(promptModal?.result ?? "");
|
|
384
|
+
setToast("Промпт скопирован");
|
|
385
|
+
setTimeout(() => setToast(""), 2500);
|
|
386
|
+
} catch {
|
|
387
|
+
setError("Не удалось скопировать — выделите текст вручную");
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
|
|
265
391
|
const presets = PRESETS;
|
|
266
392
|
const openPreset = (preset) => setForm({
|
|
267
393
|
edit: false,
|
|
@@ -333,11 +459,21 @@ window.__ModuleLoader__.load({
|
|
|
333
459
|
if (result.error) setError(result.error);
|
|
334
460
|
} catch (cause) { setError(cause instanceof Error ? cause.message : String(cause)); }
|
|
335
461
|
},
|
|
462
|
+
onPrompts: openPrompts,
|
|
336
463
|
onRemove: (id) => {
|
|
337
464
|
if (window.confirm(`Удалить MCP-сервер «${id}»?`)) void run("remove", { id }, "Сервер удалён");
|
|
338
465
|
},
|
|
339
466
|
}))) : null,
|
|
340
467
|
servers.length > 0 ? h("p", { className: "mcp-note" }, `Секреты: ${data?.secretsFile || "~/.dsh/mcp.secrets.json"} · новые tools появятся в новом чате после подключения.`) : null,
|
|
468
|
+
h(PromptModal, {
|
|
469
|
+
state: promptModal,
|
|
470
|
+
onClose: () => setPromptModal(null),
|
|
471
|
+
onPick: pickPrompt,
|
|
472
|
+
onBack: () => setPromptModal((current) => current ? { ...current, prompt: null, args: {}, result: "", error: "" } : current),
|
|
473
|
+
onArg: (name, value) => setPromptModal((current) => current ? { ...current, args: { ...current.args, [name]: value } } : current),
|
|
474
|
+
onRun: runPrompt,
|
|
475
|
+
onCopy: copyPrompt,
|
|
476
|
+
}),
|
|
341
477
|
form ? h(ServerForm, {
|
|
342
478
|
initial: form,
|
|
343
479
|
busy,
|
|
@@ -47,8 +47,8 @@ function commandCwd(ctx) {
|
|
|
47
47
|
function registerCommand(ctx, manager) {
|
|
48
48
|
ctx.commands.register({
|
|
49
49
|
name: "mcp",
|
|
50
|
-
description: "MCP-серверы: список, инструменты,
|
|
51
|
-
input: { hint: "list | trust | untrust | tools [server] | call <server> <tool> [json] | resources <server> | read <server> <uri> | reload | init" },
|
|
50
|
+
description: "MCP-серверы: список, инструменты, prompts, resources, reload",
|
|
51
|
+
input: { hint: "list | trust | untrust | tools [server] | prompts [server] | prompt <server> <name> [json] | call <server> <tool> [json] | resources <server> | read <server> <uri> | reload | init" },
|
|
52
52
|
recordInput: false,
|
|
53
53
|
async handler({ rawInput }) {
|
|
54
54
|
const input = String(rawInput ?? "").trim();
|
|
@@ -87,10 +87,10 @@ function registerCommand(ctx, manager) {
|
|
|
87
87
|
return { kind: "success", text: `MCP reload: ${results.filter((r) => r.status === "fulfilled" && r.value.client).length}/${results.length} подключено.\n${formatStatus(manager)}` };
|
|
88
88
|
}
|
|
89
89
|
const server = config.servers.find((item) => item.id === rest[0]);
|
|
90
|
-
if (["tools", "call", "resources", "read"].includes(action) && !rest[0]) {
|
|
90
|
+
if (["tools", "call", "resources", "read", "prompts", "prompt"].includes(action) && !rest[0]) {
|
|
91
91
|
return { kind: "error", text: `/mcp ${action}: укажите сервер` };
|
|
92
92
|
}
|
|
93
|
-
if (["tools", "call", "resources", "read"].includes(action) && !server) {
|
|
93
|
+
if (["tools", "call", "resources", "read", "prompts", "prompt"].includes(action) && !server) {
|
|
94
94
|
return { kind: "error", text: `MCP-сервер не найден: ${rest[0]}` };
|
|
95
95
|
}
|
|
96
96
|
if (!server.trusted) return { kind: "error", text: `MCP ${server.id}: trusted=false; включите доверие в mcp.json` };
|
|
@@ -111,6 +111,29 @@ function registerCommand(ctx, manager) {
|
|
|
111
111
|
const text = await manager.call(server.id, toolName, args);
|
|
112
112
|
return { kind: "success", text: text || "(пустой результат)" };
|
|
113
113
|
}
|
|
114
|
+
if (action === "prompts") {
|
|
115
|
+
const prompts = await manager.prompts(server.id);
|
|
116
|
+
return {
|
|
117
|
+
kind: "success",
|
|
118
|
+
text: prompts.length
|
|
119
|
+
? prompts.map((prompt) => {
|
|
120
|
+
const args = Array.isArray(prompt.arguments) ? prompt.arguments.map((arg) => `${arg.name}${arg.required ? "*" : ""}`).join(", ") : "";
|
|
121
|
+
return `- ${prompt.name}${prompt.description ? ` — ${prompt.description}` : ""}${args ? ` [${args}]` : ""}`;
|
|
122
|
+
}).join("\n")
|
|
123
|
+
: "У сервера нет prompts.",
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (action === "prompt") {
|
|
127
|
+
const name = rest[1];
|
|
128
|
+
if (!name) return { kind: "error", text: "/mcp prompt: укажите name" };
|
|
129
|
+
const args = rest[2] ? JSON.parse(rest.slice(2).join(" ")) : {};
|
|
130
|
+
const result = await manager.prompt(server.id, name, args);
|
|
131
|
+
const text = (Array.isArray(result?.messages) ? result.messages : [])
|
|
132
|
+
.map((message) => typeof message.content === "string" ? message.content : message.content?.text ?? "")
|
|
133
|
+
.filter(Boolean)
|
|
134
|
+
.join("\n");
|
|
135
|
+
return { kind: "success", text: text || "(пустой prompt)" };
|
|
136
|
+
}
|
|
114
137
|
if (action === "resources") {
|
|
115
138
|
const resources = await manager.resources(server.id);
|
|
116
139
|
return { kind: "success", text: resources.length ? resources.map((item) => `- ${item.uri}${item.name ? ` — ${item.name}` : ""}`).join("\n") : "Нет resources." };
|