dsh-edt-projects 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,27 +1,34 @@
1
1
  # dsh-edt-projects
2
2
 
3
- Бандл DeepSeek Harness для работы с проектами 1С: **1C:EDT** через
4
- [EDT-MCP](https://github.com/DitriXNew/EDT-MCP) всегда подключён, **BSL Language
5
- Server** и MCP-сервер системы задач — по требованию (`mcp_pin`). В пакет входят
6
- правила выбора инструментов для модели и навыки SonarQube и GitLab.
3
+ Плагин [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) для
4
+ разработки на 1С: держит **1C:EDT** подключённым через
5
+ [EDT-MCP](https://github.com/DitriXNew/EDT-MCP), поднимает **BSL Language Server**
6
+ по требованию, добавляет настройки на проект, правила для модели и навыки
7
+ SonarQube/GitLab.
8
+
9
+ Плагин включается только в выбранных проектах: сессия в каталоге с
10
+ `.dsh-1c.json` или в пути из `projects` файла `$DSH_HOME/dsh-1c.json`. В
11
+ остальных проектах в контексте остаётся только `edt_config`, которым проект и
12
+ включается.
7
13
 
8
14
  ## Установка
9
15
 
10
- ```
16
+ ```text
11
17
  plugin_manager { action: "install_bundle", target: "dsh-edt-projects" }
12
18
  ```
13
19
 
14
- После установки серверы `edt`, `bsl` и `tasks` настраиваются файлами
15
- `$DSH_HOME/dsh-1c.json` (пользовательские) и `<проект>/.dsh-1c.json`
16
- (проектные) — ключи описаны в
20
+ Нужны EDT 2026.1/2026.2 с установленным EDT-MCP и запущенным MCP-сервером
21
+ (по умолчанию `127.0.0.1:8765`) — подробности и настройка в
17
22
  [README репозитория](https://github.com/pravets/dsh-edt-projects#readme).
18
23
 
19
24
  ## Что внутри
20
25
 
21
- - `lib/` — хаб MCP-серверов: монтаж по требованию, правила для системного
22
- промпта, настройки на проект, провайдер навыков.
26
+ - `lib/` — хаб MCP-серверов: активация по проекту, монтаж по требованию
27
+ (`edt_pin`), настройки на проект, правила для системного промпта, провайдер
28
+ навыков.
23
29
  - `skills/` — навыки `edt-mcp-project-sonar` и `edt-mcp-project-gitlab`,
24
- включаются флагами конфига.
30
+ включаются флагами `sonar.enabled` и `gitlab.enabled` внутри включённого
31
+ проекта.
25
32
  - `cordis.patch.yml` — состав серверов, который применяет менеджер плагинов.
26
33
 
27
34
  ## Лицензия
package/cordis.patch.yml CHANGED
@@ -1,20 +1,29 @@
1
1
  # Хаб MCP-серверов для разработки на 1С.
2
2
  #
3
- # Серверы описываются здесь и подключаются плагином mcp-hub (lib/index.js):
4
- # - `lazy: true` — не занимать контекст: сервер монтируется по вызову mcp_pin
5
- # - `provision: true` — параметры запуска добираются на mcp_pin (BSL LS скачивается)
6
- # - `shared: true` — одна копия на процесс; остальные серверы монтируются
7
- # в скоуп агента (параллельные worktree не мешают друг другу)
8
- # - `unavailable` — сервер описан, но настроить его нечем (mcp_pin объяснит, чем)
3
+ # Плагин стоит в профиле один раз, но активируется только в выбранных проектах:
4
+ # - сессия в каталоге с `.dsh-1c.json` — включена;
5
+ # - каталог добавлен в `projects` файла $DSH_HOME/dsh-1c.json — включена;
6
+ # - `enabled: true`/`enabled: false` — явное решение сильнее всего.
7
+ # В выключенном проекте виден только `edt_config`; в остальных сессиях ни правил
8
+ # 1С, ни MCP-серверов нет.
9
+ #
10
+ # Серверы описываются здесь и подключаются плагином mcp-hub (lib/index.js) в
11
+ # скоупе сессии:
12
+ # - `lazy: true` — не занимать контекст: сервер монтируется по вызову edt_pin
13
+ # - `provision: true` — параметры запуска добираются на edt_pin (BSL LS скачивается)
14
+ # - `unavailable` — сервер описан, но настроить его нечем (edt_pin объяснит, чем)
15
+ # `shared` больше не работает: иначе инструменты сервера протекали бы в чужие
16
+ # проекты.
9
17
  #
10
18
  # Настройки можно переопределить без перезапуска DSH — файлами
11
19
  # $DSH_HOME/dsh-1c.json (пользователь) и <cwd>/.dsh-1c.json (проект):
12
- # { "wiki": { "paths": ["C:\\git\\dev-llm-wiki"], "entry": "wiki/index.md" },
20
+ # { "projects": ["C:\\git\\trade"],
21
+ # "wiki": { "paths": ["C:\\git\\dev-llm-wiki"], "entry": "wiki/index.md" },
13
22
  # "servers": { "edt": { "url": "http://127.0.0.1:8765/mcp" },
14
23
  # "bsl": { "root": "C:\\git\\trade\\src\\cf" } },
15
24
  # "tests": { "yaxunit": { "enabled": true, "path": "tests/yaxunit", "project": "cfe_yaxunit" },
16
25
  # "vanessa": { "enabled": true, "path": "features" } } }
17
- # После правки файла: mcp_reload (или mcp_pin заново).
26
+ # После правки файла: edt_reload (или edt_pin заново).
18
27
  # Флаги tests включают правила тестируемости: YAxUnit — закладывать тестируемость
19
28
  # и писать тесты в указанный проект, Vanessa — предлагать сценарные тесты.
20
29
  #
@@ -44,9 +53,9 @@
44
53
  paths: !!js 'process.env.DSH_1C_WIKI ? process.env.DSH_1C_WIKI.split(";").filter(Boolean) : []'
45
54
  entry: !!js 'process.env.DSH_1C_WIKI_ENTRY ?? ""'
46
55
  servers:
47
- # EDT один на процесс: одна копия, чтобы агенты не поднимали лишние сессии.
56
+ # EDT монтируется в каждой включённой сессии: это отдельное HTTP-
57
+ # подключение, а не отдельный workspace в EDT.
48
58
  edt:
49
- shared: true
50
59
  lazy: false
51
60
  transport: streamable-http
52
61
  url: !!js 'process.env.EDT_MCP_URL ?? "http://127.0.0.1:8765/mcp"'
@@ -62,7 +71,7 @@
62
71
  maxAttempts: 1000
63
72
 
64
73
  # BSL Language Server: 10 инструментов, нужны эпизодически.
65
- # provision: на mcp_pin берётся готовый jar из DSH_1C_BSL_LS_JAR, иначе из
74
+ # provision: на edt_pin берётся готовый jar из DSH_1C_BSL_LS_JAR, иначе из
66
75
  # кэша, иначе скачивается последний релиз с GitHub (sha256 сверяется).
67
76
  # Корень по умолчанию — servers.bsl.root из файла настроек проекта.
68
77
  bsl:
package/lib/config.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  // Слои настроек бандла: база из патча, пользовательский файл и файл проекта.
2
2
  //
3
3
  // Приоритет: проект > пользователь > база. Файлы перечитываются на каждый
4
- // mcp_pin/mcp_reload и на каждый рендер промпта, поэтому правки применяются
4
+ // edt_pin/edt_reload и на каждый рендер промпта, поэтому правки применяются
5
5
  // без перезапуска DSH; разбор кэшируется по mtimeMs.
6
6
  import { existsSync, readFileSync, statSync } from 'node:fs';
7
7
  import { homedir } from 'node:os';
@@ -92,6 +92,17 @@ export function resolveConfig({ cwd, env = process.env, base = {} } = {}) {
92
92
  return { config: mergeLayers(base, user.config, project.config), sources, errors };
93
93
  }
94
94
 
95
+ /**
96
+ * Есть ли у проекта разбираемый файл `.dsh-1c.json`.
97
+ * Наличие самого файла при включении плагина учитывается только вместе с
98
+ * успешным разбором: битый JSON не должен включать 1С-режим.
99
+ */
100
+ export function projectConfigLoaded(cwd) {
101
+ const path = projectConfigPath(cwd);
102
+ if (path === undefined) return false;
103
+ return readJsonConfig(path).config !== undefined;
104
+ }
105
+
95
106
  /** Сбрасывает кэш — используется тестами. */
96
107
  export function clearConfigCache() {
97
108
  cache.clear();
package/lib/enable.mjs ADDED
@@ -0,0 +1,91 @@
1
+ // Включён ли плагин для проекта сессии.
2
+ //
3
+ // Плагин установлен в профиль один раз, но его инструменты, правила, навыки и
4
+ // MCP-серверы нужны только там, где работают с 1С. Поэтому активация считается
5
+ // от исходного каталога сессии:
6
+ //
7
+ // 1. `enabled: true` — включён;
8
+ // 2. `enabled: false` — выключен (сильнее файла проекта и списка);
9
+ // 3. разбираемый `.dsh-1c.json` в каталоге проекта — включён;
10
+ // 4. каталог попадает в `projects` пользовательского файла — включён;
11
+ // 5. иначе выключен.
12
+ //
13
+ // Сравнение путей: абсолютные пути, `~` раскрывается, хвостовой разделитель
14
+ // снимается, существующие пути приводятся через realpath; на Windows регистр не
15
+ // важен. Совпадение — равенство или вложенность по границе каталога, поэтому
16
+ // `C:\git\trade2` не считается проектом `C:\git\trade`.
17
+ import { realpathSync } from 'node:fs';
18
+ import { homedir } from 'node:os';
19
+ import path from 'node:path';
20
+
21
+ const implFor = (platform) => (platform === 'win32' ? path.win32 : path.posix);
22
+
23
+ /**
24
+ * Приводит путь проекта к сравнимому виду.
25
+ * @param value путь из настроек или каталог сессии.
26
+ * @param options.platform платформа для правил путей (по умолчанию текущая).
27
+ * @param options.home домашний каталог для `~`.
28
+ * @returns нормализованный абсолютный путь или undefined, если путь не абсолютный.
29
+ */
30
+ export function normalizeProjectPath(value, { platform = process.platform, home = homedir() } = {}) {
31
+ if (typeof value !== 'string') return undefined;
32
+ const impl = implFor(platform);
33
+ let text = value.trim();
34
+ if (text === '') return undefined;
35
+
36
+ if (text === '~') text = home;
37
+ else if (text.startsWith('~/') || text.startsWith('~\\')) text = impl.join(home, text.slice(2));
38
+
39
+ if (!impl.isAbsolute(text)) return undefined;
40
+ let resolved = impl.normalize(text);
41
+
42
+ // realpath имеет смысл только для реальной платформы: путь чужой платформы
43
+ // здесь не существует и был бы искажён.
44
+ if (platform === process.platform) {
45
+ try {
46
+ resolved = realpathSync.native(resolved);
47
+ } catch {
48
+ // Каталога может ещё не быть — сравниваем нормализованный путь.
49
+ }
50
+ }
51
+
52
+ // Хвостовой разделитель не должен делать «C:\git\trade\» другим проектом.
53
+ // Корень диска и «/» сохраняются как есть.
54
+ const trimmed = resolved.replace(/[\\/]+$/, '');
55
+ if (trimmed !== '') resolved = trimmed;
56
+ if (platform === 'win32') resolved = resolved.toLowerCase();
57
+ return resolved;
58
+ }
59
+
60
+ /** Совпадает ли каталог с записью списка проектов (сам каталог или его потомок). */
61
+ export function matchesProject(cwd, entry, options = {}) {
62
+ const platform = options.platform ?? process.platform;
63
+ const left = normalizeProjectPath(cwd, { ...options, platform });
64
+ const right = normalizeProjectPath(entry, { ...options, platform });
65
+ if (left === undefined || right === undefined) return false;
66
+ if (left === right) return true;
67
+ const impl = implFor(platform);
68
+ const base = right.endsWith(impl.sep) ? right : right + impl.sep;
69
+ return left.startsWith(base);
70
+ }
71
+
72
+ /**
73
+ * Решение об активации вместе с причиной — причина нужна edt_config show.
74
+ * @returns {{ enabled: boolean, reason: string, entry?: string }}
75
+ */
76
+ export function activationFor({ cwd, config, hasProjectFile = false } = {}, options = {}) {
77
+ const value = config ?? {};
78
+ if (value.enabled === true) return { enabled: true, reason: 'enabled' };
79
+ if (value.enabled === false) return { enabled: false, reason: 'disabled' };
80
+ if (hasProjectFile) return { enabled: true, reason: 'project-file' };
81
+
82
+ const projects = Array.isArray(value.projects) ? value.projects : [];
83
+ const hit = projects.find((entry) => matchesProject(cwd, entry, options));
84
+ if (hit !== undefined) return { enabled: true, reason: 'allowlist', entry: hit };
85
+ return { enabled: false, reason: 'not-selected' };
86
+ }
87
+
88
+ /** Короткая форма activationFor для гейтов в рантайме. */
89
+ export function isEnabledFor(input, options = {}) {
90
+ return activationFor(input, options).enabled;
91
+ }
package/lib/index.js CHANGED
@@ -1,30 +1,36 @@
1
1
  // Хаб MCP-серверов для DSH: подключение по требованию, настройки на проект
2
2
  // и правила для модели.
3
3
  //
4
+ // Плагин стоит в профиле один раз, но активируется только в выбранных проектах:
5
+ // правила 1С, собственные инструменты и MCP-серверы появляются лишь там, где
6
+ // проект отмечен `.dsh-1c.json`, `enabled: true` или попадает в `projects`
7
+ // пользовательского файла (см. lib/enable.mjs). В остальных сессиях виден
8
+ // только `edt_config`, которым проект и включается.
9
+ //
4
10
  // Зачем: каждый подключённый сервер отдаёт свои схемы инструментов в КАЖДЫЙ
5
- // запрос к модели. EDT (99 инструментов) нужен постоянно, а BSL Language Server
6
- // и система задач — эпизодически. Поэтому сервер помечается `lazy: true` и
7
- // монтируется только по вызову `mcp_pin`; `mcp_unpin` возвращает контекст.
11
+ // запрос к модели. EDT (99 инструментов) нужен постоянно в 1С-проекте, а BSL
12
+ // Language Server и система задач — эпизодически. Поэтому серверы монтируются в
13
+ // скоуп сессии: `lazy: true` — только по вызову `edt_pin`; незаленивые — при
14
+ // создании сессии включённого проекта. `edt_unpin` возвращает контекст.
8
15
  //
9
16
  // Настройки: база из cordis.patch.yml <- $DSH_HOME/dsh-1c.json <- <cwd>/.dsh-1c.json.
10
- // Файлы перечитываются на каждый pin/рендер промпта, поэтому правки применяются
11
- // через mcp_reload без перезапуска DSH.
12
- //
13
- // Серверы описываются в config.servers (см. cordis.patch.yml):
14
- // edt: { shared: true, transport: streamable-http, url, headers, lazy: false }
15
- // bsl: { lazy: true, provision: true, root } — jar берётся из env/кэша
16
- // tasks: { lazy: true, transport: streamable-http, url, headers }
17
+ // Файлы перечитываются на каждый pin/рендер промпта и на каждый шаг (для
18
+ // включения на лету), поэтому правки применяются через edt_reload без
19
+ // перезапуска DSH.
17
20
  //
18
- // `shared: true` — одна копия на процесс (EDT один workspace); остальные серверы
19
- // монтируются в скоуп агента, поэтому параллельные агенты в разных worktree
20
- // получают каждый свой BSL LS со своим корнем.
21
+ // `shared: true` больше не работает: сервер всегда живёт в скоупе сессии, иначе
22
+ // его инструменты протекали бы в чужие проекты.
21
23
  export const name = 'mcp-hub';
22
24
 
23
25
  export const inject = ['tools', 'workingDirectory'];
24
26
 
27
+ /** Собственные инструменты плагина; префикс edt_ отделяет их от `mcp__edt__*`. */
28
+ export const TOOL_NAMES = ['edt_servers', 'edt_pin', 'edt_unpin', 'edt_reload', 'edt_config'];
29
+
25
30
  import { watch } from 'node:fs';
26
31
  import { bslAvailable, bslSettings, ensureJar, resolveBsl, startBslSupervisor } from './bsl.mjs';
27
- import { projectConfigPath, resolveConfig, userConfigPath } from './config.mjs';
32
+ import { projectConfigLoaded, projectConfigPath, resolveConfig, userConfigPath } from './config.mjs';
33
+ import { activationFor } from './enable.mjs';
28
34
  import { normalizeWiki, targetPathOf, writeDenialReason } from './wiki.mjs';
29
35
  import { normalizeTestTooling } from './tests.mjs';
30
36
  import { initFile, upgradeFile } from './config-file.mjs';
@@ -33,7 +39,7 @@ import { createSkillsProvider } from './skills.mjs';
33
39
 
34
40
  const HEADER = `## Работа с 1С через MCP
35
41
 
36
- К этому профилю подключены MCP-серверы для работы с конфигурацией 1С. Правила:
42
+ К этому проекту подключены MCP-серверы для работы с конфигурацией 1С. Правила:
37
43
 
38
44
  - Структуру, метаданные и код ищи через MCP, а не файловым поиском. Чтение файлов и \`grep\` — запасной путь, когда MCP-инструмент недоступен или уже вернул ошибку.
39
45
  - Проверяй утверждения о коде тем же инструментом, которым исследовал. Не выдавай непроверенное за проверенное.
@@ -43,12 +49,12 @@ const HEADER = `## Работа с 1С через MCP
43
49
  const PIN_RULES = `
44
50
  ### Серверы по требованию
45
51
 
46
- Часть серверов не подключена заранее, чтобы не занимать контекст их схемами. Посмотреть состояние — \`mcp_servers\`; подключить — \`mcp_pin(server)\`, отключить — \`mcp_unpin(server)\`.
52
+ Часть серверов не подключена заранее, чтобы не занимать контекст их схемами. Посмотреть состояние — \`edt_servers\`; подключить — \`edt_pin(server)\`, отключить — \`edt_unpin(server)\`.
47
53
 
48
54
  - Подключай сервер сам, когда задача без него не решается: нужна диагностика или типы — \`bsl\`, нужны задачи или статусы — \`tasks\`.
49
- - Для \`bsl\` передавай корень: \`mcp_pin("bsl", root: "<каталог конфигурации>")\`. Без корня берётся корень из настроек проекта, иначе каталог запуска DSH.
50
- - Первый \`mcp_pin("bsl")\` может занять время: при отсутствии готового jar сервер скачивается (~130 МБ). Предупреди пользователя, если это долго.
51
- - После \`mcp_unpin\` инструменты сервера исчезают из запроса. Не отключай сервер, пока работа с ним не закончена.
55
+ - Для \`bsl\` передавай корень: \`edt_pin("bsl", root: "<каталог конфигурации>")\`. Без корня берётся корень из настроек проекта, иначе каталог запуска DSH.
56
+ - Первый \`edt_pin("bsl")\` может занять время: при отсутствии готового jar сервер скачивается (~130 МБ). Предупреди пользователя, если это долго.
57
+ - После \`edt_unpin\` инструменты сервера исчезают из запроса. Не отключай сервер, пока работа с ним не закончена.
52
58
  - Если сервер помечен как не настроенный, не пытайся его подключить: скажи пользователю, чего не хватает.`;
53
59
 
54
60
  const EDT_EDIT_RULES = `
@@ -82,7 +88,7 @@ const BSL_RULES = `
82
88
  - \`type_info\` и \`global_member_info\` — свойства, методы и сигнатуры типов и глобального контекста платформы. Сверяйся с ними, а не по памяти.
83
89
  - \`definition\`, \`find_references\`, \`call_hierarchy\`, \`hover\`, \`type_at_position\`, \`document_symbols\` — навигация. Перед изменением метода посмотри \`call_hierarchy\` или \`find_references\`.
84
90
  - Позиции в \`line\`/\`character\` нумеруются с нуля.
85
- - Файлы вне рабочего каталога сервера дают \`File is not part of any registered workspace\`. Тогда переподключи сервер с нужным корнем: \`mcp_pin("bsl", root: "<каталог конфигурации>")\` — путь проекта видно в \`edt__list_projects\`.`;
91
+ - Файлы вне рабочего каталога сервера дают \`File is not part of any registered workspace\`. Тогда переподключи сервер с нужным корнем: \`edt_pin("bsl", root: "<каталог конфигурации>")\` — путь проекта видно в \`edt__list_projects\`.`;
86
92
 
87
93
  const TASKS_RULES = `
88
94
  ### Система задач (\`tasks__*\`)
@@ -199,7 +205,6 @@ export function normalizeServers(config = {}) {
199
205
  transport,
200
206
  lazy: raw.lazy === true,
201
207
  unavailable: raw.unavailable === true,
202
- shared: raw.shared === true,
203
208
  raw,
204
209
  });
205
210
  }
@@ -269,9 +274,24 @@ const textTool = (toolName, description, parameters, execute) => ({
269
274
  execute,
270
275
  });
271
276
 
272
- /** Подпись конфигурации для обнаружения изменений при mcp_reload. */
277
+ /** Подпись конфигурации для обнаружения изменений при edt_reload. */
273
278
  const signatureOf = (entry, root) => JSON.stringify({ config: mountConfig(entry), root });
274
279
 
280
+ const activationReason = (activation) => {
281
+ switch (activation.reason) {
282
+ case 'enabled':
283
+ return 'enabled: true в настройках';
284
+ case 'disabled':
285
+ return 'enabled: false в настройках';
286
+ case 'project-file':
287
+ return 'есть .dsh-1c.json проекта';
288
+ case 'allowlist':
289
+ return `путь в projects (${activation.entry})`;
290
+ default:
291
+ return 'проект не выбран: нет .dsh-1c.json и его нет в projects';
292
+ }
293
+ };
294
+
275
295
  /**
276
296
  * Каталог, от которого считаются настройки проекта, корень BSL и пути вики.
277
297
  * Исходный каталог сессии (`session.header.cwd`) неизменяем, поэтому `cd`
@@ -299,11 +319,12 @@ export async function apply(ctx, config) {
299
319
  const agentStates = new WeakMap();
300
320
  const watchedProjects = new Set();
301
321
  const loggedErrors = new Set();
322
+ let skillsControl;
302
323
 
303
324
  const cwdOf = (agent) => sessionCwd(agent, ctx.workingDirectory);
304
325
 
305
- const effectiveFor = (agent) => {
306
- const effective = resolveConfig({ cwd: cwdOf(agent), env: process.env, base });
326
+ const effectiveForCwd = (cwd) => {
327
+ const effective = resolveConfig({ cwd, env: process.env, base });
307
328
  for (const error of effective.errors) {
308
329
  if (loggedErrors.has(error)) continue;
309
330
  loggedErrors.add(error);
@@ -312,6 +333,13 @@ export async function apply(ctx, config) {
312
333
  return effective;
313
334
  };
314
335
 
336
+ const effectiveFor = (agent) => effectiveForCwd(cwdOf(agent));
337
+
338
+ const activationOfCwd = (cwd) =>
339
+ activationFor({ cwd, config: effectiveForCwd(cwd).config, hasProjectFile: projectConfigLoaded(cwd) });
340
+
341
+ const activationOf = (agent) => activationOfCwd(cwdOf(agent));
342
+
315
343
  const entriesFor = (effective) => {
316
344
  const bsl = bslSettingsFor(effective);
317
345
  return normalizeServers(effective.config?.servers).map((entry) => {
@@ -324,22 +352,23 @@ export async function apply(ctx, config) {
324
352
  });
325
353
  };
326
354
 
327
- const mapFor = (agent, shared) => {
328
- if (shared) return globalState;
329
- if (agent === undefined) return globalState;
330
- let map = agentStates.get(agent);
331
- if (map === undefined) {
332
- map = new Map();
333
- agentStates.set(agent, map);
355
+ /** Состояние активации одного агента (сессии): инструменты и смонтированные серверы. */
356
+ const stateOf = (agent) => {
357
+ let state = agentStates.get(agent);
358
+ if (state === undefined) {
359
+ state = { active: false, disposers: [], items: new Map() };
360
+ agentStates.set(agent, state);
334
361
  }
335
- return map;
362
+ return state;
336
363
  };
337
364
 
365
+ const mapFor = (agent) => (agent === undefined ? globalState : stateOf(agent).items);
366
+
338
367
  const itemFor = (agent, entry) => {
339
- const map = mapFor(agent, entry.shared);
368
+ const map = mapFor(agent);
340
369
  let item = map.get(entry.name);
341
370
  if (item === undefined) {
342
- item = { entry, fiber: null, error: null, supervisor: null, signature: undefined };
371
+ item = { entry, fiber: null, error: null, supervisor: null, signature: undefined, suppressed: false };
343
372
  map.set(entry.name, item);
344
373
  } else {
345
374
  item.entry = entry;
@@ -347,10 +376,13 @@ export async function apply(ctx, config) {
347
376
  return item;
348
377
  };
349
378
 
350
- const toolNamesOf = (serverName) => {
379
+ const toolNamesOf = (agent, serverName) => {
351
380
  const prefix = `mcp__${serverName}__`;
352
381
  try {
353
- return [...ctx.tools.view(undefined).visible.keys()].filter((toolName) => toolName.startsWith(prefix));
382
+ return ctx.tools
383
+ .schemas(agent === undefined ? undefined : agent)
384
+ .map((schema) => schema.name)
385
+ .filter((toolName) => toolName.startsWith(prefix));
354
386
  } catch {
355
387
  return [];
356
388
  }
@@ -359,8 +391,7 @@ export async function apply(ctx, config) {
359
391
  const snapshot = (agent) => {
360
392
  const effective = effectiveFor(agent);
361
393
  return entriesFor(effective).map((entry) => {
362
- const map = mapFor(agent, entry.shared);
363
- const item = map.get(entry.name);
394
+ const item = mapFor(agent).get(entry.name);
364
395
  let value = 'lazy';
365
396
  if (entry.unavailable) value = 'unavailable';
366
397
  if (item?.fiber && !item.error) value = 'mounted';
@@ -368,11 +399,11 @@ export async function apply(ctx, config) {
368
399
  return {
369
400
  name: entry.name,
370
401
  state: value,
371
- scope: entry.shared ? 'shared' : agent === undefined ? 'global' : 'agent',
402
+ scope: agent === undefined ? 'global' : 'session',
372
403
  transport: item?.supervisor ? 'lsp+mcp' : entry.transport,
373
404
  root: item?.supervisor?.root ?? entry.raw?.root,
374
405
  endpoint: item?.supervisor?.url ?? entry.raw?.url,
375
- tools: toolNamesOf(entry.name).length,
406
+ tools: toolNamesOf(agent, entry.name).length,
376
407
  error: item?.error,
377
408
  };
378
409
  });
@@ -383,7 +414,7 @@ export async function apply(ctx, config) {
383
414
  await item.fiber.dispose().catch(() => {});
384
415
  item.fiber = null;
385
416
  }
386
- await item.supervisor?.stop();
417
+ await Promise.resolve(item.supervisor?.stop?.()).catch(() => {});
387
418
  item.supervisor = null;
388
419
  item.error = null;
389
420
  item.signature = undefined;
@@ -406,7 +437,10 @@ export async function apply(ctx, config) {
406
437
 
407
438
  if (item.fiber && !item.error) {
408
439
  const sameRoot = item.supervisor === null || item.supervisor?.root === root;
409
- if (sameRoot) return { already: true, tools: toolNamesOf(serverName).length };
440
+ if (sameRoot) {
441
+ item.suppressed = false;
442
+ return { already: true, tools: toolNamesOf(agent, serverName).length };
443
+ }
410
444
  await disposeItem(item);
411
445
  }
412
446
 
@@ -425,6 +459,13 @@ export async function apply(ctx, config) {
425
459
  const jar = await ensureJar(bsl, { log, warn });
426
460
  const supervisor = await startBslSupervisor({ jar, java: bsl.java, root, port: bsl.port, log });
427
461
  item.supervisor = supervisor;
462
+ // Процесс BSL LS должен умереть вместе со скоупом агента, даже если
463
+ // disposeItem не вызвали (сессия закрыта, плагин выгружен).
464
+ if (agent !== undefined) {
465
+ agent.ctx.effect(() => () => {
466
+ Promise.resolve(supervisor.stop()).catch(() => {});
467
+ });
468
+ }
428
469
  resolved = { transport: 'streamable-http', url: supervisor.url };
429
470
  }
430
471
  resolvedEntry = {
@@ -439,33 +480,39 @@ export async function apply(ctx, config) {
439
480
  };
440
481
  } catch (error) {
441
482
  item.error = error instanceof Error ? error.message : String(error);
442
- await item.supervisor?.stop();
483
+ await Promise.resolve(item.supervisor?.stop?.()).catch(() => {});
443
484
  item.supervisor = null;
444
485
  return { already: false, tools: 0, error: item.error };
445
486
  }
446
487
  }
447
488
 
448
489
  const McpClient = await import('@deepseek-ai/dsh-mcp-client');
449
- const scopeCtx = entry.shared || agent === undefined ? ctx : agent.ctx;
490
+ const scopeCtx = agent === undefined ? ctx : agent.ctx;
450
491
  const fiber = scopeCtx.plugin(McpClient, mountConfig(resolvedEntry));
451
492
  item.fiber = fiber;
452
493
  item.error = null;
453
494
  item.signature = signatureOf(resolvedEntry, root);
495
+ item.suppressed = false;
454
496
  try {
455
497
  await fiber;
456
498
  } catch (error) {
457
499
  item.error = error instanceof Error ? error.message : String(error);
458
500
  }
459
- return { already: false, tools: toolNamesOf(serverName).length, error: item.error };
501
+ return { already: false, tools: toolNamesOf(agent, serverName).length, error: item.error };
460
502
  };
461
503
 
462
504
  const unmount = async (agent, serverName) => {
463
505
  const effective = effectiveFor(agent);
464
506
  const entry = entriesFor(effective).find((candidate) => candidate.name === serverName);
465
- const map = entry === undefined ? mapFor(agent, false) : mapFor(agent, entry.shared);
507
+ const map = mapFor(agent);
466
508
  const item = map.get(serverName);
467
- if (item === undefined || !item.fiber) return false;
509
+ if (item === undefined || !item.fiber) {
510
+ if (entry !== undefined && item !== undefined) item.suppressed = true;
511
+ return false;
512
+ }
468
513
  await disposeItem(item);
514
+ // Незаленивый сервер иначе смонтируется снова на следующем шаге.
515
+ item.suppressed = true;
469
516
  return true;
470
517
  };
471
518
 
@@ -480,8 +527,7 @@ export async function apply(ctx, config) {
480
527
 
481
528
  const changed = [];
482
529
  for (const entry of targets) {
483
- const map = mapFor(agent, entry.shared);
484
- const item = map.get(entry.name);
530
+ const item = mapFor(agent).get(entry.name);
485
531
  if (item === undefined || !item.fiber) continue;
486
532
  const bsl = bslSettingsFor(effective);
487
533
  const root = entry.raw.root ?? bsl.root;
@@ -496,7 +542,98 @@ export async function apply(ctx, config) {
496
542
 
497
543
  // Если плагин выгружают, процессы серверов не должны остаться висеть.
498
544
  ctx.effect(() => () => {
499
- for (const item of globalState.values()) item.supervisor?.stop();
545
+ for (const item of globalState.values()) Promise.resolve(item.supervisor?.stop?.()).catch(() => {});
546
+ });
547
+
548
+ // --- активация на проект ----------------------------------------------------
549
+ const invalidateSkills = () => {
550
+ try {
551
+ skillsControl?.invalidate?.();
552
+ } catch {
553
+ // Инвалидация каталога — вспомогательная операция, не валим шаг.
554
+ }
555
+ };
556
+
557
+ const watchFile = (path) => {
558
+ if (path === undefined || watchedProjects.has(path)) return;
559
+ watchedProjects.add(path);
560
+ try {
561
+ const watcher = watch(path, { persistent: false }, () => {
562
+ invalidateSkills();
563
+ ctx.logger?.warn?.(`mcp-hub: настройки изменились (${path}). Примените их через edt_reload.`);
564
+ });
565
+ watcher.on('error', () => {});
566
+ ctx.effect(() => () => watcher.close());
567
+ } catch {
568
+ // Файла может ещё не быть — наблюдение не критично.
569
+ }
570
+ };
571
+
572
+ const activate = async (agent) => {
573
+ const state = stateOf(agent);
574
+ if (state.active) return;
575
+ state.active = true;
576
+ for (const definition of [serversTool(), pinTool(), unpinTool(), reloadTool()]) {
577
+ state.disposers.push(agent.ctx.tools.register(definition));
578
+ }
579
+ watchFile(projectConfigPath(cwdOf(agent)));
580
+
581
+ const effective = effectiveFor(agent);
582
+ for (const entry of entriesFor(effective)) {
583
+ if (entry.lazy || entry.unavailable) continue;
584
+ try {
585
+ await mount(agent, entry.name);
586
+ } catch (error) {
587
+ ctx.logger?.warn?.(`mcp-hub: ${entry.name} failed to mount: ${error instanceof Error ? error.message : String(error)}`);
588
+ }
589
+ }
590
+ };
591
+
592
+ const deactivate = async (agent) => {
593
+ const state = agentStates.get(agent);
594
+ if (state === undefined || !state.active) return;
595
+ state.active = false;
596
+ for (const dispose of state.disposers.splice(0)) {
597
+ try {
598
+ dispose();
599
+ } catch {
600
+ // Инструменты могли быть уже сняты выгрузкой скоупа.
601
+ }
602
+ }
603
+ for (const item of state.items.values()) await disposeItem(item);
604
+ state.items.clear();
605
+ };
606
+
607
+ /**
608
+ * Сводит состояние сессии с текущими настройками: включает проект на лету
609
+ * после edt_config init/правки файла и выключает после enabled:false.
610
+ */
611
+ const reconcile = async (agent) => {
612
+ if (agent === undefined) return;
613
+ const { enabled } = activationOf(agent);
614
+ const state = agentStates.get(agent);
615
+ if (enabled) {
616
+ if (state === undefined || !state.active) await activate(agent);
617
+ return;
618
+ }
619
+ await deactivate(agent);
620
+ };
621
+
622
+ ctx.on('agent/created', async ({ agent }) => {
623
+ try {
624
+ await reconcile(agent);
625
+ } catch (error) {
626
+ ctx.logger?.warn?.(`mcp-hub: активация для сессии не удалась: ${error instanceof Error ? error.message : String(error)}`);
627
+ }
628
+ });
629
+
630
+ ctx.on('agent/pre-step', async ({ agent }, next) => {
631
+ try {
632
+ await reconcile(agent);
633
+ } catch (error) {
634
+ ctx.logger?.warn?.(`mcp-hub: активация для сессии не удалась: ${error instanceof Error ? error.message : String(error)}`);
635
+ }
636
+ return next();
500
637
  });
501
638
 
502
639
  // --- вики: чтение разрешено, запись отклоняется ---
@@ -509,10 +646,10 @@ export async function apply(ctx, config) {
509
646
  return writeDenialReason(exec.name, target, roots, { cwd });
510
647
  });
511
648
 
512
- ctx.tools.register(
649
+ const serversTool = () =>
513
650
  textTool(
514
- 'mcp_servers',
515
- 'List MCP servers: state (mounted, lazy, unavailable, failed), scope, transport, tool count, workspace root and endpoint. Settings are read from $DSH_HOME/dsh-1c.json and <cwd>/.dsh-1c.json.',
651
+ 'edt_servers',
652
+ 'List MCP servers of the 1C plugin: state (mounted, lazy, unavailable, failed), scope, transport, tool count, workspace root and endpoint. Settings are read from $DSH_HOME/dsh-1c.json and <cwd>/.dsh-1c.json.',
516
653
  {},
517
654
  async (_args, exec) => {
518
655
  const rows = snapshot(exec?.agent).map((item) => {
@@ -527,11 +664,10 @@ export async function apply(ctx, config) {
527
664
  });
528
665
  return { text: rows.length > 0 ? rows.join('\n') : 'No MCP servers are configured.' };
529
666
  },
530
- ),
531
- );
667
+ );
532
668
 
533
669
  const serverParam = {
534
- server: { type: 'string', description: 'Server name as reported by mcp_servers.', required: true },
670
+ server: { type: 'string', description: 'Server name as reported by edt_servers.', required: true },
535
671
  root: {
536
672
  type: 'string',
537
673
  description:
@@ -540,50 +676,48 @@ export async function apply(ctx, config) {
540
676
  mode: { type: 'string', description: 'For a provisioned server: "hybrid" (LSP+MCP with workspace roots, default) or "stdio".' },
541
677
  };
542
678
 
543
- ctx.tools.register(
679
+ const pinTool = () =>
544
680
  textTool(
545
- 'mcp_pin',
681
+ 'edt_pin',
546
682
  'Mount a configured MCP server now, so its tools appear in this session. Re-reads the settings files first, so edited ports or roots apply without restarting DSH.',
547
683
  serverParam,
548
684
  async (args, exec) => {
549
685
  const result = await mount(exec?.agent, args.server, { root: args.root, mode: args.mode });
550
- if (result.error) throw new Error(`mcp_pin(${args.server}) failed: ${result.error}`);
686
+ if (result.error) throw new Error(`edt_pin(${args.server}) failed: ${result.error}`);
551
687
  if (result.already) return { text: `${args.server} is already mounted (${result.tools} tools).` };
552
688
  return { text: `${args.server} mounted (${result.tools} tools). They are available from the next step.` };
553
689
  },
554
- ),
555
- );
690
+ );
556
691
 
557
- ctx.tools.register(
692
+ const unpinTool = () =>
558
693
  textTool(
559
- 'mcp_unpin',
694
+ 'edt_unpin',
560
695
  'Unmount a mounted MCP server and remove its tools from the session, freeing prompt space. Do not unmount a server while work with it is in progress.',
561
696
  { server: serverParam.server },
562
697
  async (args, exec) => {
563
698
  const wasMounted = await unmount(exec?.agent, args.server);
564
699
  return { text: wasMounted ? `${args.server} unmounted.` : `${args.server} was not mounted.` };
565
700
  },
566
- ),
567
- );
701
+ );
568
702
 
569
- ctx.tools.register(
703
+ const reloadTool = () =>
570
704
  textTool(
571
- 'mcp_reload',
705
+ 'edt_reload',
572
706
  'Re-read the settings files ($DSH_HOME/dsh-1c.json and <cwd>/.dsh-1c.json) and restart mounted servers whose url, headers, command or root changed. Use after editing a settings file; no DSH restart needed.',
573
707
  { server: { type: 'string', description: 'Only this server; omit to check every configured server.' } },
574
708
  async (args, exec) => {
709
+ invalidateSkills();
575
710
  const changed = await reload(exec?.agent, args.server);
576
711
  if (changed.length === 0) return { text: 'Настройки перечитаны, изменений в подключённых серверах нет.' };
577
712
  const rows = changed.map((item) => `${item.name}: ${item.error === undefined ? 'перезапущен' : `ошибка — ${item.error}`}`);
578
713
  return { text: `Настройки перечитаны:\n${rows.join('\n')}` };
579
714
  },
580
- ),
581
- );
715
+ );
582
716
 
583
- ctx.tools.register(
717
+ const configTool = () =>
584
718
  textTool(
585
- 'mcp_config',
586
- 'Inspect or maintain the settings file. action=show prints the effective settings, their sources and validation warnings; init creates the file with defaults (refuses when it exists); upgrade adds only missing keys, keeping your values; validate reports type errors and unknown keys.',
719
+ 'edt_config',
720
+ 'Manage dsh-edt-projects settings: which projects enable the 1C plugin (enabled/projects), wiki paths, test tooling, MCP servers and bundled skills. action=show prints the effective settings, their sources, whether this project is enabled and validation warnings; init creates the project .dsh-1c.json (which enables the plugin for it); upgrade adds only missing keys, keeping your values; validate reports type errors and unknown keys.',
587
721
  {
588
722
  action: {
589
723
  type: 'string',
@@ -597,12 +731,14 @@ export async function apply(ctx, config) {
597
731
  async (args, exec) => {
598
732
  const cwd = cwdOf(exec?.agent);
599
733
  const path = args.scope === 'user' ? userConfigPath(process.env) : projectConfigPath(cwd);
600
- if (path === undefined) throw new Error('mcp_config: cannot resolve the settings path');
734
+ if (path === undefined) throw new Error('edt_config: cannot resolve the settings path');
601
735
 
602
736
  if (args.action === 'show') {
603
737
  const effective = effectiveFor(exec?.agent);
604
738
  const { errors, warnings } = validateConfig(effective.config);
739
+ const activation = activationOf(exec?.agent);
605
740
  const head = [
741
+ `Проект: ${activation.enabled ? 'включён' : 'выключен'} — ${activationReason(activation)}`,
606
742
  `Источники: ${effective.sources.length > 0 ? effective.sources.join(', ') : 'только cordis.patch.yml'}`,
607
743
  `Действующие настройки:\n${stringifyConfig(effective.config)}`,
608
744
  ];
@@ -614,15 +750,21 @@ export async function apply(ctx, config) {
614
750
  if (args.action === 'init') {
615
751
  const created = initFile(path);
616
752
  if (created.status === 'exists') return { text: `${path} уже существует — используйте action=upgrade, чтобы дополнить его.` };
617
- if (created.status === 'error') throw new Error(`mcp_config init: ${created.error}`);
618
- return { text: `Создан ${path} со значениями по умолчанию. Дальше впишите пути вики, корень конфигурации и флаги тестов.` };
753
+ if (created.status === 'error') throw new Error(`edt_config init: ${created.error}`);
754
+ invalidateSkills();
755
+ const hint =
756
+ args.scope === 'user'
757
+ ? 'Дальше впишите пути проектов в projects, чтобы включить плагин там.'
758
+ : 'Проект включён со следующего шага: появятся правила 1С и mcp__edt__*.';
759
+ return { text: `Создан ${path}. ${hint}` };
619
760
  }
620
761
 
621
762
  if (args.action === 'upgrade') {
622
763
  const dryRun = args.dryRun !== false;
623
764
  const result = upgradeFile(path, { dryRun });
624
- if (result.status === 'error') throw new Error(`mcp_config upgrade: ${result.error}`);
765
+ if (result.status === 'error') throw new Error(`edt_config upgrade: ${result.error}`);
625
766
  if (result.status === 'noop') return { text: `${path}: актуален, добавлять нечего.` };
767
+ if (!dryRun) invalidateSkills();
626
768
  const lines = [`${path}: ${dryRun ? 'будет добавлено' : 'добавлено'} ${result.added.length} ключ(ей):`];
627
769
  for (const key of result.added) lines.push(` + ${key}`);
628
770
  if (dryRun) lines.push('Это был предварительный просмотр. Повторите с dryRun: false, чтобы записать.');
@@ -640,35 +782,25 @@ export async function apply(ctx, config) {
640
782
  for (const warning of warnings) lines.push(` ! ${warning}`);
641
783
  return { text: lines.join('\n') };
642
784
  },
643
- ),
644
- );
785
+ );
786
+
787
+ // edt_config остаётся глобальным: это единственная точка входа, которой
788
+ // включают проект, поэтому он не может зависеть от активации.
789
+ ctx.tools.register(configTool());
645
790
 
646
791
  // --- навыки, поставляемые с бандлом ---
647
- // Навык появляется в каталоге только когда включён в конфиге проекта:
648
- // list() провайдера возвращает пустой список при выключенном флаге.
792
+ // Провайдер глобальный, но отдаёт навыки только для включённого проекта;
793
+ // инвалидация каталога при правке настроек — через control.invalidate().
649
794
  ctx.inject(['skills'], (inner) => {
650
- inner.skills.registerProvider(() =>
651
- createSkillsProvider({
795
+ inner.skills.registerProvider((control) => {
796
+ skillsControl = control;
797
+ return createSkillsProvider({
652
798
  resolveConfig: (cwd) => resolveConfig({ cwd: cwd ?? process.cwd(), env: process.env, base }).config,
799
+ isEnabled: (cwd, config) => activationFor({ cwd, config, hasProjectFile: projectConfigLoaded(cwd) }).enabled,
653
800
  logger: ctx.logger,
654
- }),
655
- );
656
- });
657
-
658
- // --- наблюдение за файлами настроек ---
659
- const watchFile = (path) => {
660
- if (path === undefined || watchedProjects.has(path)) return;
661
- watchedProjects.add(path);
662
- try {
663
- const watcher = watch(path, { persistent: false }, () => {
664
- ctx.logger?.warn?.(`mcp-hub: настройки изменились (${path}). Примените их через mcp_reload.`);
665
801
  });
666
- watcher.on('error', () => {});
667
- ctx.effect(() => () => watcher.close());
668
- } catch {
669
- // Файла может ещё не быть — наблюдение не критично.
670
- }
671
- };
802
+ });
803
+ });
672
804
 
673
805
  watchFile(userConfigPath(process.env));
674
806
  watchFile(projectConfigPath(cwdOf(undefined)));
@@ -680,25 +812,13 @@ export async function apply(ctx, config) {
680
812
  interpolate: false,
681
813
  text: (context) => {
682
814
  const agent = context?.agent;
815
+ // В выключенном проекте секция пустая: renderPrompt выбрасывает её,
816
+ // поэтому в промпте не остаётся ни строчки про 1С.
817
+ if (agent === undefined || !activationOf(agent).enabled) return '';
683
818
  const effective = effectiveFor(agent);
684
819
  const { roots } = normalizeWiki(effective.config?.wiki, { cwd: cwdOf(agent) });
685
820
  return buildRulesText(process.env, snapshot(agent), roots, effective.config?.tests);
686
821
  },
687
822
  });
688
823
  });
689
-
690
- // Серверы без `lazy` подключаются на старте и ЖДУТСЯ: иначе их инструменты
691
- // опаздывают к первому запросу и модель видит пустой набор MCP.
692
- // Ошибка подключения не валит загрузку: сервер останется в состоянии failed,
693
- // а mcp-client продолжит реконнект в фоне.
694
- const eager = entriesFor(effectiveFor(undefined)).filter((entry) => !entry.lazy && !entry.unavailable);
695
- await Promise.all(
696
- eager.map(async (entry) => {
697
- try {
698
- await mount(undefined, entry.name);
699
- } catch (error) {
700
- ctx.logger?.warn?.(`mcp-hub: ${entry.name} failed to mount: ${error instanceof Error ? error.message : String(error)}`);
701
- }
702
- }),
703
- );
704
824
  }
package/lib/schema.mjs CHANGED
@@ -3,12 +3,15 @@
3
3
  //
4
4
  // Правила апгрейда: существующие значения не меняются, недостающие ключи
5
5
  // добавляются, неизвестные ключи сохраняются, версия поднимается.
6
- export const CONFIG_VERSION = 1;
6
+ export const CONFIG_VERSION = 2;
7
7
 
8
8
  /** Значения по умолчанию для нового файла. Пустые строки и массивы
9
- * нормализаторы игнорируют, поэтому init ничего не включает сам по себе. */
9
+ * нормализаторы игнорируют, поэтому init ничего не включает сам по себе.
10
+ * `enabled` намеренно не входит в defaults: `upgrade` не должен выключать
11
+ * проект, который включён самим фактом своего `.dsh-1c.json`. */
10
12
  export const DEFAULTS = {
11
13
  version: CONFIG_VERSION,
14
+ projects: [],
12
15
  wiki: { paths: [], entry: '' },
13
16
  servers: {
14
17
  edt: { url: 'http://127.0.0.1:8765/mcp' },
@@ -26,15 +29,17 @@ export const DEFAULTS = {
26
29
  /** Известные ключи: путь -> описание и тип. Используются валидацией и README. */
27
30
  export const KNOWN = {
28
31
  version: { type: 'number', description: 'Версия схемы настроек.' },
32
+ enabled: { type: 'boolean', description: 'Включить или выключить плагин для этого проекта (сильнее файла проекта и списка projects).' },
33
+ projects: { type: 'array', description: 'Каталоги проектов, где плагин включён (пользовательский файл).' },
29
34
  'wiki.paths': { type: 'array', description: 'Каталоги вики (абсолютные или относительно проекта).' },
30
35
  'wiki.entry': { type: 'string', description: 'Входная точка вики, например wiki/index.md.' },
31
36
  'servers': { type: 'object', description: 'Настройки MCP-серверов по именам.' },
32
37
  'servers.*.url': { type: 'string', description: 'Адрес MCP-сервера (streamable-http).' },
33
38
  'servers.*.headers': { type: 'object', description: 'Заголовки запросов, например Authorization.' },
34
39
  'servers.*.root': { type: 'string', description: 'Корень рабочего пространства (для bsl).' },
35
- 'servers.*.lazy': { type: 'boolean', description: 'Не подключать на старте, только по mcp_pin.' },
36
- 'servers.*.shared': { type: 'boolean', description: 'Одна копия на процесс вместо скоупа агента.' },
37
- 'servers.*.provision': { type: 'boolean', description: 'Параметры запуска добираются на mcp_pin.' },
40
+ 'servers.*.lazy': { type: 'boolean', description: 'Не подключать на старте, только по edt_pin.' },
41
+ 'servers.*.shared': { type: 'boolean', description: 'Устарело: серверы всегда монтируются в скоупе сессии.' },
42
+ 'servers.*.provision': { type: 'boolean', description: 'Параметры запуска добираются на edt_pin.' },
38
43
  'servers.*.toolCallTimeoutMs': { type: 'number', description: 'Таймаут вызова инструмента.' },
39
44
  'servers.*.failOnStartupError': { type: 'boolean', description: 'Падать, если стартовое подключение не удалось.' },
40
45
  'servers.*.reconnect': { type: 'object', description: 'Политика переподключения.' },
@@ -171,6 +176,12 @@ export function validateConfig(config) {
171
176
  if (!config.wiki.paths.every((item) => typeof item === 'string')) errors.push('wiki.paths: ожидался массив строк');
172
177
  }
173
178
 
179
+ if (config.projects !== undefined && !Array.isArray(config.projects)) {
180
+ errors.push(`projects: ожидался массив путей, получено ${typeof config.projects}`);
181
+ } else if (Array.isArray(config.projects) && !config.projects.every((item) => typeof item === 'string')) {
182
+ errors.push('projects: ожидался массив строк');
183
+ }
184
+
174
185
  return { errors, warnings };
175
186
  }
176
187
 
package/lib/skills.mjs CHANGED
@@ -63,15 +63,24 @@ export function skillFile(name) {
63
63
  /**
64
64
  * Создаёт провайдер навыков.
65
65
  * @param options.resolveConfig вызывается с cwd и возвращает действующий конфиг.
66
+ * @param options.isEnabled вызывается с (cwd, config) и говорит, активирован ли
67
+ * плагин для этого проекта; для выключенного проекта навыков нет.
66
68
  * @param options.logger необязательный логгер для предупреждений.
67
69
  */
68
- export function createSkillsProvider({ resolveConfig, logger } = {}) {
70
+ export function createSkillsProvider({ resolveConfig, isEnabled, logger } = {}) {
69
71
  const read = (name) => {
70
72
  const path = skillFile(name);
71
73
  if (!existsSync(path)) return undefined;
72
74
  return { path, ...parseFrontmatter(readFileSync(path, 'utf8')) };
73
75
  };
74
76
 
77
+ const configFor = (cwd) => (typeof resolveConfig === 'function' ? resolveConfig(cwd) : {}) ?? {};
78
+
79
+ const allowed = (cwd) => {
80
+ if (typeof isEnabled !== 'function') return true;
81
+ return isEnabled(cwd, configFor(cwd)) === true;
82
+ };
83
+
75
84
  const toCandidate = (name) => {
76
85
  const parsed = read(name);
77
86
  if (parsed === undefined || parsed.meta.description === undefined) return undefined;
@@ -91,7 +100,9 @@ export function createSkillsProvider({ resolveConfig, logger } = {}) {
91
100
  name: 'edt-mcp-bundle-skills',
92
101
  /** Отдаёт только навыки, включённые в конфиге проекта. */
93
102
  list(options = {}) {
94
- const config = typeof resolveConfig === 'function' ? resolveConfig(options.cwd) : {};
103
+ const cwd = options.cwd ?? process.cwd();
104
+ if (!allowed(cwd)) return [];
105
+ const config = configFor(cwd);
95
106
  const candidates = [];
96
107
  for (const skill of SKILLS) {
97
108
  if (!skill.enabled(config)) continue;
@@ -105,7 +116,8 @@ export function createSkillsProvider({ resolveConfig, logger } = {}) {
105
116
  return candidates;
106
117
  },
107
118
  /** Возвращает тело навыка без frontmatter. */
108
- get(candidate) {
119
+ get(candidate, options = {}) {
120
+ if (!allowed(options.cwd ?? process.cwd())) return undefined;
109
121
  const parsed = read(candidate?.name);
110
122
  if (parsed === undefined) return undefined;
111
123
  return {
package/locale/en.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "meta": {
3
3
  "title": "1C:EDT + BSL MCP",
4
- "description": "MCP hub for 1C development: EDT is always connected; BSL Language Server (diagnostics, types, navigation) and a task tracker are mounted on demand via mcp_pin. Adds tool-selection rules to the prompt."
4
+ "description": "MCP hub for 1C development: active only in selected projects (.dsh-1c.json or the projects list). There EDT is connected up front, while BSL Language Server (diagnostics, types, navigation) and a task tracker are mounted on demand via edt_pin; 1C rules enter the prompt only there."
5
5
  }
6
6
  }
package/locale/ru.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "meta": {
3
3
  "title": "1C:EDT + BSL MCP",
4
- "description": "Хаб MCP-серверов для разработки на 1С: EDT подключён всегда, BSL Language Server (диагностика, типы, навигация) и система задач — по требованию через mcp_pin. Добавляет в промпт правила выбора инструментов."
4
+ "description": "Хаб MCP-серверов для разработки на 1С: включается только в выбранных проектах (файл .dsh-1c.json или список projects). В них EDT подключён сразу, BSL Language Server (диагностика, типы, навигация) и система задач — по требованию через edt_pin; правила 1С появляются в промпте только там."
5
5
  }
6
6
  }
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "dsh-edt-projects",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
- "description": "MCP hub for 1C development: EDT always on, BSL Language Server started in LSP+MCP mode with workspace roots, task tracker via mcp_pin, bundled SonarQube and GitLab skills, with model-facing usage rules",
5
+ "description": "Project-scoped MCP hub for 1C development: active only in selected projects, EDT connected up front there, BSL Language Server and a task tracker mounted on demand, bundled SonarQube and GitLab skills, with model-facing usage rules",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "git+https://github.com/pravets/dsh-edt-projects.git",
10
- "directory": "plugin"
10
+ "directory": "packages/dsh-edt-projects"
11
11
  },
12
12
  "homepage": "https://github.com/pravets/dsh-edt-projects#readme",
13
13
  "bugs": {