dsh-edt-projects 0.9.1 → 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 +10 -3
- package/cordis.patch.yml +20 -11
- package/lib/config.mjs +12 -1
- package/lib/enable.mjs +91 -0
- package/lib/index.js +234 -114
- package/lib/schema.mjs +16 -5
- package/lib/skills.mjs +15 -3
- package/locale/en.json +1 -1
- package/locale/ru.json +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
по требованию, добавляет настройки на проект, правила для модели и навыки
|
|
7
7
|
SonarQube/GitLab.
|
|
8
8
|
|
|
9
|
+
Плагин включается только в выбранных проектах: сессия в каталоге с
|
|
10
|
+
`.dsh-1c.json` или в пути из `projects` файла `$DSH_HOME/dsh-1c.json`. В
|
|
11
|
+
остальных проектах в контексте остаётся только `edt_config`, которым проект и
|
|
12
|
+
включается.
|
|
13
|
+
|
|
9
14
|
## Установка
|
|
10
15
|
|
|
11
16
|
```text
|
|
@@ -18,10 +23,12 @@ plugin_manager { action: "install_bundle", target: "dsh-edt-projects" }
|
|
|
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
|
-
включаются флагами `sonar.enabled` и `gitlab.enabled
|
|
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
|
-
#
|
|
4
|
-
# -
|
|
5
|
-
# -
|
|
6
|
-
# - `
|
|
7
|
-
#
|
|
8
|
-
#
|
|
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
|
-
# { "
|
|
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
|
-
# После правки файла:
|
|
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: на
|
|
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
|
-
//
|
|
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 инструментов) нужен
|
|
6
|
-
// и система задач — эпизодически. Поэтому
|
|
7
|
-
//
|
|
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
|
-
// через
|
|
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`
|
|
19
|
-
//
|
|
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
|
-
К этому
|
|
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
|
-
Часть серверов не подключена заранее, чтобы не занимать контекст их схемами. Посмотреть состояние — \`
|
|
52
|
+
Часть серверов не подключена заранее, чтобы не занимать контекст их схемами. Посмотреть состояние — \`edt_servers\`; подключить — \`edt_pin(server)\`, отключить — \`edt_unpin(server)\`.
|
|
47
53
|
|
|
48
54
|
- Подключай сервер сам, когда задача без него не решается: нужна диагностика или типы — \`bsl\`, нужны задачи или статусы — \`tasks\`.
|
|
49
|
-
- Для \`bsl\` передавай корень: \`
|
|
50
|
-
- Первый \`
|
|
51
|
-
- После \`
|
|
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\`. Тогда переподключи сервер с нужным корнем: \`
|
|
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
|
-
/** Подпись конфигурации для обнаружения изменений при
|
|
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
|
|
306
|
-
const effective = resolveConfig({ cwd
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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)
|
|
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 =
|
|
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 =
|
|
507
|
+
const map = mapFor(agent);
|
|
466
508
|
const item = map.get(serverName);
|
|
467
|
-
if (item === undefined || !item.fiber)
|
|
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
|
|
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
|
-
|
|
649
|
+
const serversTool = () =>
|
|
513
650
|
textTool(
|
|
514
|
-
'
|
|
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
|
|
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
|
-
|
|
679
|
+
const pinTool = () =>
|
|
544
680
|
textTool(
|
|
545
|
-
'
|
|
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(`
|
|
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
|
-
|
|
692
|
+
const unpinTool = () =>
|
|
558
693
|
textTool(
|
|
559
|
-
'
|
|
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
|
-
|
|
703
|
+
const reloadTool = () =>
|
|
570
704
|
textTool(
|
|
571
|
-
'
|
|
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
|
-
|
|
717
|
+
const configTool = () =>
|
|
584
718
|
textTool(
|
|
585
|
-
'
|
|
586
|
-
'
|
|
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('
|
|
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(`
|
|
618
|
-
|
|
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(`
|
|
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
|
-
//
|
|
792
|
+
// Провайдер глобальный, но отдаёт навыки только для включённого проекта;
|
|
793
|
+
// инвалидация каталога при правке настроек — через control.invalidate().
|
|
649
794
|
ctx.inject(['skills'], (inner) => {
|
|
650
|
-
inner.skills.registerProvider(() =>
|
|
651
|
-
|
|
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
|
-
|
|
667
|
-
|
|
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 =
|
|
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: 'Не подключать на старте, только по
|
|
36
|
-
'servers.*.shared': { type: 'boolean', description: '
|
|
37
|
-
'servers.*.provision': { type: 'boolean', description: 'Параметры запуска добираются на
|
|
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
|
|
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
|
|
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 подключён
|
|
4
|
+
"description": "Хаб MCP-серверов для разработки на 1С: включается только в выбранных проектах (файл .dsh-1c.json или список projects). В них EDT подключён сразу, BSL Language Server (диагностика, типы, навигация) и система задач — по требованию через edt_pin; правила 1С появляются в промпте только там."
|
|
5
5
|
}
|
|
6
6
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-edt-projects",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "MCP hub for 1C development:
|
|
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",
|