dsh-desktop-kit 2.7.6 → 2.7.7

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 CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.7.7 (2026-11-12)
4
+
5
+ - **MCP prompts.** Settings → MCP → **Prompts**: список промптов сервера, аргументы, запуск, результат и копирование. Новые RPC `prompts`/`prompt`; клиент поддерживает `prompts/list` и `prompts/get` с пагинацией.
6
+ - **`/mcp prompts` и `/mcp prompt`.** Промпты доступны и из командной строки, включая JSON-аргументы.
7
+ - **UI.** Модалка prompts проверена на 390/768/1440: overflow 0, console 0, markup 0.
8
+ - **README.** Скриншоты prompts 1440/390 и описание.
9
+
3
10
  ## 2.7.6 (2026-11-11)
4
11
 
5
12
  - **README images через unpkg.** Npm-страница не разрешает относительные пути, а GitHub raw и jsDelivr в этой сети не открываются. Все 15 изображений переведены на `https://unpkg.com/dsh-desktop-kit@2.7.4/...`; загрузка проверена в headless Chrome.
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  ## MCP: подключения без JSON
10
10
 
11
- Настройки → **MCP**: пресеты GitHub, PostgreSQL и Filesystem, живые статусы, health, список tools и безопасное хранение токенов в `~/.dsh/mcp.secrets.json`.
11
+ Настройки → **MCP**: пресеты GitHub, PostgreSQL и Filesystem, живые статусы, health, список tools и безопасное хранение токенов в `~/.dsh/mcp.secrets.json`. Промпты сервера открываются кнопкой **Prompts**: аргументы, запуск и копирование результата.
12
12
 
13
13
  ![MCP Settings](https://unpkg.com/dsh-desktop-kit@2.7.4/plugins/dsh-mcp-manager/screenshots/settings-1440.png)
14
14
 
@@ -25,7 +25,7 @@
25
25
 
26
26
  ![Агенты проекта](https://unpkg.com/dsh-desktop-kit@2.7.4/plugins/dsh-agent-extensions/screenshots/agents-1440.png)
27
27
 
28
- **v2.7.6** · MIT · проверено на DSH `0.2.1-rc.5` и `0.2.2-rc.1` (Windows 11) · без внешних зависимостей и телеметрии
28
+ **v2.7.7** · MIT · проверено на DSH `0.2.1-rc.5` и `0.2.2-rc.1` (Windows 11) · без внешних зависимостей и телеметрии
29
29
 
30
30
  **План развития:** [docs/ROADMAP-3.0.md](docs/ROADMAP-3.0.md) — Memory Fabric, семантическое
31
31
  слияние, локальный индекс памяти, шифрованные переносы, Memory Hub и npm trusted publishing.
package/client.js CHANGED
@@ -6929,6 +6929,12 @@ var(--dsw-alias-bg-layer-1,#fff);box-shadow:inset 0 1px 0 color-mix(in srgb,#fff
6929
6929
  .mcp-tool{display:flex;gap:8px;align-items:baseline;font-size:12px;line-height:1.45;min-width:0}
6930
6930
  .mcp-tool code{color:var(--mcp-accent);font:inherit;font-weight:650;white-space:nowrap}
6931
6931
  .mcp-tool span{color:var(--mcp-muted);overflow:hidden;text-overflow:ellipsis}
6932
+ .mcp-prompt-list{display:flex;flex-direction:column;gap:8px}
6933
+ .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}
6934
+ .mcp-prompt-card:hover{border-color:rgba(15,118,110,.4);background:var(--mcp-accent-soft)}
6935
+ .mcp-prompt-card b{display:block;font-size:13px}
6936
+ .mcp-prompt-card span{display:block;color:var(--mcp-muted);font-size:12px;line-height:1.4;margin-top:3px}
6937
+ .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}
6932
6938
  .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}
6933
6939
  .mcp-empty h3{margin:0;font-size:18px;letter-spacing:-.015em}
6934
6940
  .mcp-empty p{margin:0;color:var(--mcp-muted);font-size:13px;line-height:1.5;max-width:60ch}
@@ -6983,7 +6989,7 @@ var(--dsw-alias-bg-layer-1,#fff);box-shadow:inset 0 1px 0 color-mix(in srgb,#fff
6983
6989
  );
6984
6990
  }
6985
6991
 
6986
- function ServerCard({ server, busy, onToggle, onTest, onTools, onRemove, tools }) {
6992
+ function ServerCard({ server, busy, onToggle, onTest, onTools, onPrompts, onRemove, tools }) {
6987
6993
  const [open, setOpen] = useState(false);
6988
6994
  const connected = server.connected;
6989
6995
  const status = connected ? "on" : (server.error ? "err" : "off");
@@ -7006,6 +7012,7 @@ var(--dsw-alias-bg-layer-1,#fff);box-shadow:inset 0 1px 0 color-mix(in srgb,#fff
7006
7012
  h(Toggle, { checked: server.trusted, label: "Доверен", disabled: busy, onChange: (value) => onToggle(server.id, { trusted: value }) }),
7007
7013
  h("div", { className: "mcp-actions" },
7008
7014
  h("button", { className: "mcp-btn ghost", disabled: busy, onClick: () => onTest(server.id) }, "Проверить"),
7015
+ h("button", { className: "mcp-btn ghost", disabled: busy, onClick: () => onPrompts(server.id) }, "Prompts"),
7009
7016
  h("button", { className: "mcp-btn ghost", disabled: busy, onClick: () => { setOpen(!open); if (!open) onTools(server.id); } }, open ? "Скрыть" : "Tools"),
7010
7017
  h("button", { className: "mcp-btn danger ghost", disabled: busy, onClick: () => onRemove(server.id) }, "Удалить"),
7011
7018
  ),
@@ -7021,6 +7028,58 @@ var(--dsw-alias-bg-layer-1,#fff);box-shadow:inset 0 1px 0 color-mix(in srgb,#fff
7021
7028
  );
7022
7029
  }
7023
7030
 
7031
+ function PromptModal({ state, onClose, onPick, onBack, onArg, onRun, onCopy }) {
7032
+ if (!state) return null;
7033
+ const prompt = state.prompt;
7034
+ const args = Array.isArray(prompt?.arguments) ? prompt.arguments : [];
7035
+ return h("div", { className: "mcp-overlay", onClick: onClose },
7036
+ h("div", {
7037
+ className: "mcp-modal",
7038
+ role: "dialog",
7039
+ "aria-modal": "true",
7040
+ "aria-labelledby": "mcp-prompts-title",
7041
+ onClick: (event) => event.stopPropagation(),
7042
+ },
7043
+ h("div", { className: "mcp-modal-head" },
7044
+ h("div", null,
7045
+ h("h2", { id: "mcp-prompts-title" }, prompt ? (prompt.title || prompt.name) : "Prompts"),
7046
+ h("p", null, prompt ? (prompt.description || "Промпт MCP-сервера") : `Промпты сервера «${state.serverId}».`),
7047
+ ),
7048
+ h("button", { className: "mcp-btn ghost", type: "button", onClick: onClose, "aria-label": "Закрыть" }, "Закрыть"),
7049
+ ),
7050
+ state.error ? h("div", { className: "mcp-error", role: "alert" }, state.error) : null,
7051
+ !prompt ? h("div", { className: "mcp-prompt-list" },
7052
+ state.prompts.length === 0 ? h("div", { className: "mcp-meta" }, "У сервера нет prompts.") : state.prompts.map((item) => h("button", {
7053
+ key: item.name,
7054
+ type: "button",
7055
+ className: "mcp-prompt-card",
7056
+ onClick: () => onPick(item),
7057
+ }, h("b", null, item.name), h("span", null, item.description || "Без описания"))),
7058
+ ) : h("div", { className: "mcp-form" },
7059
+ args.map((arg, index) => h("div", { className: `mcp-field${String(arg.description || "").length > 40 ? " wide" : ""}`, key: arg.name },
7060
+ h("label", { htmlFor: `mcp-prompt-arg-${index}` }, arg.name, arg.required ? " *" : ""),
7061
+ h("input", {
7062
+ id: `mcp-prompt-arg-${index}`,
7063
+ value: state.args[arg.name] ?? "",
7064
+ placeholder: arg.description || "",
7065
+ onChange: (event) => onArg(arg.name, event.target.value),
7066
+ }),
7067
+ )),
7068
+ args.length === 0 ? h("p", { className: "mcp-note", style: { gridColumn: "1/-1" } }, "У промпта нет аргументов.") : null,
7069
+ state.result ? h("div", { className: "mcp-field wide" },
7070
+ h("label", null, "Результат"),
7071
+ h("pre", { className: "mcp-prompt-output" }, state.result),
7072
+ ) : null,
7073
+ h("div", { className: "mcp-form-actions", style: { gridColumn: "1/-1" } },
7074
+ state.prompts.length > 1 ? h("button", { className: "mcp-btn ghost", type: "button", onClick: onBack }, "Назад") : null,
7075
+ state.result ? h("button", { className: "mcp-btn ghost", type: "button", onClick: onCopy }, "Копировать") : null,
7076
+ h("button", { className: "mcp-btn primary", type: "button", disabled: state.busy, onClick: onRun }, state.busy ? "Запрашиваем…" : "Получить"),
7077
+ ),
7078
+ ),
7079
+ ),
7080
+ );
7081
+ }
7082
+
7024
7083
  function ServerForm({ initial, busy, onCancel, onSave }) {
7025
7084
  const [form, setForm] = useState(initial);
7026
7085
  const set = (patch) => setForm((current) => ({ ...current, ...patch }));
@@ -7100,6 +7159,7 @@ var(--dsw-alias-bg-layer-1,#fff);box-shadow:inset 0 1px 0 color-mix(in srgb,#fff
7100
7159
  const [form, setForm] = useState(null);
7101
7160
  const [tools, setTools] = useState({});
7102
7161
  const [toast, setToast] = useState("");
7162
+ const [promptModal, setPromptModal] = useState(null);
7103
7163
 
7104
7164
  const load = async () => {
7105
7165
  try {
@@ -7127,6 +7187,72 @@ var(--dsw-alias-bg-layer-1,#fff);box-shadow:inset 0 1px 0 color-mix(in srgb,#fff
7127
7187
  }
7128
7188
  };
7129
7189
 
7190
+ const openPrompts = async (id) => {
7191
+ setBusy(true);
7192
+ setError(null);
7193
+ try {
7194
+ const result = await call(window.__DSH_MCP_CTX__, "prompts", { id });
7195
+ setPromptModal({
7196
+ serverId: id,
7197
+ prompts: Array.isArray(result.prompts) ? result.prompts : [],
7198
+ prompt: null,
7199
+ args: {},
7200
+ result: "",
7201
+ busy: false,
7202
+ error: result.error || "",
7203
+ });
7204
+ } catch (cause) {
7205
+ setError(cause instanceof Error ? cause.message : String(cause));
7206
+ } finally {
7207
+ setBusy(false);
7208
+ }
7209
+ };
7210
+
7211
+ const pickPrompt = (prompt) => {
7212
+ setPromptModal((current) => current ? {
7213
+ ...current,
7214
+ prompt,
7215
+ args: Object.fromEntries((prompt.arguments ?? []).map((arg) => [arg.name, ""])),
7216
+ result: "",
7217
+ error: "",
7218
+ } : current);
7219
+ };
7220
+
7221
+ const runPrompt = async () => {
7222
+ const current = promptModal;
7223
+ if (!current?.prompt) return;
7224
+ setPromptModal({ ...current, busy: true, error: "", result: "" });
7225
+ try {
7226
+ const result = await call(window.__DSH_MCP_CTX__, "prompt", {
7227
+ id: current.serverId,
7228
+ name: current.prompt.name,
7229
+ arguments: current.args,
7230
+ });
7231
+ setPromptModal((latest) => latest ? {
7232
+ ...latest,
7233
+ busy: false,
7234
+ result: result.text || "",
7235
+ error: result.text ? "" : "Сервер вернул пустой промпт",
7236
+ } : latest);
7237
+ } catch (cause) {
7238
+ setPromptModal((latest) => latest ? {
7239
+ ...latest,
7240
+ busy: false,
7241
+ error: cause instanceof Error ? cause.message : String(cause),
7242
+ } : latest);
7243
+ }
7244
+ };
7245
+
7246
+ const copyPrompt = async () => {
7247
+ try {
7248
+ await navigator.clipboard.writeText(promptModal?.result ?? "");
7249
+ setToast("Промпт скопирован");
7250
+ setTimeout(() => setToast(""), 2500);
7251
+ } catch {
7252
+ setError("Не удалось скопировать — выделите текст вручную");
7253
+ }
7254
+ };
7255
+
7130
7256
  const presets = PRESETS;
7131
7257
  const openPreset = (preset) => setForm({
7132
7258
  edit: false,
@@ -7198,11 +7324,21 @@ var(--dsw-alias-bg-layer-1,#fff);box-shadow:inset 0 1px 0 color-mix(in srgb,#fff
7198
7324
  if (result.error) setError(result.error);
7199
7325
  } catch (cause) { setError(cause instanceof Error ? cause.message : String(cause)); }
7200
7326
  },
7327
+ onPrompts: openPrompts,
7201
7328
  onRemove: (id) => {
7202
7329
  if (window.confirm(`Удалить MCP-сервер «${id}»?`)) void run("remove", { id }, "Сервер удалён");
7203
7330
  },
7204
7331
  }))) : null,
7205
7332
  servers.length > 0 ? h("p", { className: "mcp-note" }, `Секреты: ${data?.secretsFile || "~/.dsh/mcp.secrets.json"} · новые tools появятся в новом чате после подключения.`) : null,
7333
+ h(PromptModal, {
7334
+ state: promptModal,
7335
+ onClose: () => setPromptModal(null),
7336
+ onPick: pickPrompt,
7337
+ onBack: () => setPromptModal((current) => current ? { ...current, prompt: null, args: {}, result: "", error: "" } : current),
7338
+ onArg: (name, value) => setPromptModal((current) => current ? { ...current, args: { ...current.args, [name]: value } } : current),
7339
+ onRun: runPrompt,
7340
+ onCopy: copyPrompt,
7341
+ }),
7206
7342
  form ? h(ServerForm, {
7207
7343
  initial: form,
7208
7344
  busy,
@@ -7402,7 +7538,7 @@ var(--dsw-alias-bg-layer-1,#fff);box-shadow:inset 0 1px 0 color-mix(in srgb,#fff
7402
7538
  if (__kitErrors.length <= 5) console.error("[dsh-desktop-kit] registration failed:", label, message);
7403
7539
  };
7404
7540
  try {
7405
- window.__DSH_DESKTOP_KIT__ = { version: "2.7.6", parts: __partNames, errors: __kitErrors, ready: false };
7541
+ window.__DSH_DESKTOP_KIT__ = { version: "2.7.7", parts: __partNames, errors: __kitErrors, ready: false };
7406
7542
  } catch {}
7407
7543
  const __inject = [];
7408
7544
  for (const __part of __parts) {
@@ -0,0 +1,25 @@
1
+ # DSH Desktop Kit 2.7.7 — тест
2
+
3
+ Дата: 2026-11-12.
4
+
5
+ ## MCP prompts
6
+
7
+ - `McpClient.listPrompts()` — `prompts/list` с пагинацией, только при capability `prompts`.
8
+ - `McpClient.getPrompt()` — `prompts/get` с аргументами.
9
+ - `McpManager.prompts/prompt`.
10
+ - RPC Settings: `prompts`, `prompt`.
11
+ - Команды: `/mcp prompts <server>`, `/mcp prompt <server> <name> [json]`.
12
+ - UI: модалка со списком, полями аргументов, результатом и копированием.
13
+
14
+ ## Проверки
15
+
16
+ - `node tools/mcp-manager-tests.mjs` — OK, включая prompts/list и prompts/get.
17
+ - `npm test` — exit 0.
18
+ - `npm run build:client` / `check:client` — OK.
19
+ - browser_qa audit 390/768/1440 — overflow 0, console 0, markup 0.
20
+ - Скриншоты: `plugins/dsh-mcp-manager/screenshots/prompts-1440.png`, `prompts-390.png`.
21
+
22
+ ## Ограничения
23
+
24
+ - OAuth и sampling по-прежнему не реализованы.
25
+ - Аргументы промпта — строковые поля; вложенные объекты задаются через `/mcp prompt` JSON.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-desktop-kit",
3
- "version": "2.7.6",
3
+ "version": "2.7.7",
4
4
  "description": "DeepSeek Harness desktop kit 2.7: 16 plugins and 30 skills — self-applying skills, token lab, Skills Studio, project memory, GitHub and USB memory transfer, guardrails, repo map, checkpoints, plan mode, browser QA, GitHub review, Claude Code-like hooks/agents/commands, Russian UI, peak guard, chat move and profile transfer.",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -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
  ![MCP Settings](screenshots/settings-1440.png)
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
+ ![MCP Prompts](screenshots/prompts-1440.png)
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
- - Нет UI: управление через `/mcp` и JSON-файлы.
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-серверы: список, инструменты, вызов, resources, reload",
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." };
@@ -32,8 +32,17 @@ async function readEnvelope(request) {
32
32
  return { rpcId: body.rpcId, method: body.method, payload: body.payload ?? {} };
33
33
  }
34
34
 
35
- function publicServer(server) {
36
- return {
35
+ function promptText(result) {
36
+ const messages = Array.isArray(result?.messages) ? result.messages : [];
37
+ return messages.map((message) => {
38
+ const content = message?.content;
39
+ if (typeof content === "string") return content;
40
+ if (Array.isArray(content)) return content.map((block) => block?.text ?? "").filter(Boolean).join("\n");
41
+ return content?.text ?? "";
42
+ }).filter(Boolean).join("\n\n");
43
+ }
44
+
45
+ function publicServer(server) { return {
37
46
  id: server.id,
38
47
  scope: server.scope,
39
48
  transport: server.url ? "http" : "stdio",
@@ -239,6 +248,28 @@ export function registerApi(ctx, manager) {
239
248
  error: record.error,
240
249
  };
241
250
  });
251
+ operation("prompts", async (payload) => {
252
+ const config = loadConfig({ cwd: process.cwd() });
253
+ const server = config.servers.find((item) => item.id === String(payload.id ?? ""));
254
+ if (!server) throw new Error("server not found");
255
+ const record = await manager.connect(server);
256
+ if (!record.client?.connected) return { prompts: [], error: record.error };
257
+ try {
258
+ const prompts = await manager.prompts(server.id);
259
+ record.prompts = prompts;
260
+ return { prompts, error: "" };
261
+ } catch (error) {
262
+ return { prompts: [], error: error instanceof Error ? error.message : String(error) };
263
+ }
264
+ });
265
+ operation("prompt", async (payload) => {
266
+ const config = loadConfig({ cwd: process.cwd() });
267
+ const server = config.servers.find((item) => item.id === String(payload.id ?? ""));
268
+ if (!server) throw new Error("server not found");
269
+ await manager.connect(server);
270
+ const result = await manager.prompt(server.id, String(payload.name ?? ""), payload.arguments ?? {});
271
+ return { text: promptText(result), description: result?.description ?? "" };
272
+ });
242
273
  operation("call", async (payload) => {
243
274
  const config = loadConfig({ cwd: process.cwd() });
244
275
  const server = config.servers.find((item) => item.id === String(payload.id ?? ""));
@@ -165,6 +165,7 @@ export class McpClient {
165
165
  this.capabilities = {};
166
166
  this.tools = [];
167
167
  this.resources = [];
168
+ this.prompts = [];
168
169
  }
169
170
 
170
171
  async connect() {
@@ -212,6 +213,23 @@ export class McpClient {
212
213
  return this.transport.request("resources/read", { uri }, this.config.timeoutMs);
213
214
  }
214
215
 
216
+ async listPrompts() {
217
+ if (!this.capabilities.prompts) return [];
218
+ const prompts = [];
219
+ let cursor;
220
+ do {
221
+ const result = await this.transport.request("prompts/list", cursor ? { cursor } : {}, this.config.timeoutMs);
222
+ if (Array.isArray(result?.prompts)) prompts.push(...result.prompts);
223
+ cursor = result?.nextCursor;
224
+ } while (cursor);
225
+ this.prompts = prompts;
226
+ return prompts;
227
+ }
228
+
229
+ async getPrompt(name, args = {}) {
230
+ return this.transport.request("prompts/get", { name, arguments: args }, this.config.timeoutMs);
231
+ }
232
+
215
233
  async close() {
216
234
  await this.transport.close?.();
217
235
  this.connected = false;
@@ -53,7 +53,7 @@ export class McpManager {
53
53
  throw new Error("MCP manager is closed");
54
54
  }
55
55
  const tools = await client.listTools();
56
- const record = { server, client, tools, resources: [], error: "" };
56
+ const record = { server, client, tools, resources: [], prompts: [], error: "" };
57
57
  this.clients.set(server.id, record);
58
58
  for (const tool of tools) {
59
59
  const name = this.toolName(server.id, tool.name);
@@ -62,7 +62,7 @@ export class McpManager {
62
62
  return record;
63
63
  } catch (error) {
64
64
  await client.close().catch(() => {});
65
- const record = { server, client: null, tools: [], resources: [], error: error instanceof Error ? error.message : String(error) };
65
+ const record = { server, client: null, tools: [], resources: [], prompts: [], error: error instanceof Error ? error.message : String(error) };
66
66
  this.clients.set(server.id, record);
67
67
  return record;
68
68
  }
@@ -122,6 +122,18 @@ export class McpManager {
122
122
  return record.client.readResource(uri);
123
123
  }
124
124
 
125
+ async prompts(serverId) {
126
+ const record = this.clients.get(serverId);
127
+ if (!record?.client?.connected) throw new Error(`MCP server not connected: ${serverId}`);
128
+ return record.client.listPrompts();
129
+ }
130
+
131
+ async prompt(serverId, name, args) {
132
+ const record = this.clients.get(serverId);
133
+ if (!record?.client?.connected) throw new Error(`MCP server not connected: ${serverId}`);
134
+ return record.client.getPrompt(name, args);
135
+ }
136
+
125
137
  status() {
126
138
  return [...this.clients.values()].map((record) => ({
127
139
  id: record.server.id,