Убраны интероп-сборки из поставки сервера, добавлен KompasInteropLoader
This commit is contained in:
@@ -1,50 +0,0 @@
|
||||
---
|
||||
name: docs-maintainer
|
||||
description: "ОБЯЗАТЕЛЬНО делегируй этому субагенту (Sonnet) ВСЮ синхронизацию документации проекта с кодом — README.md, CLAUDE.md, docs/ARCHITECTURE.md, docs/OPEN_QUESTIONS.md, docs/presentation.html — НЕ правь эти файлы сам. Срабатывает ПРОАКТИВНО как обязательный шаг рабочего цикла, а НЕ по явной команде: как только фича реализована и протестирована, ПЕРЕД коммитом доков передай ему сводку изменений (новые/переименованные/удалённые инструменты, классы, факты реализации, закрытые вопросы OPEN_QUESTIONS, новые счётчики инструментов/тестов для presentation.html). Не жди реплики «обнови доки» — делегируй сам после каждой завершённой единицы работы. Триггеры (включая ТВОИ внутренние): «фича готова, надо отразить в доках», «обнови README/ARCHITECTURE/CLAUDE.md/presentation», «закрыт вопрос в OPEN_QUESTIONS», «синхронизируй документацию», «задокументируй изменения». Передавай сводку текстом — код/тесты он сам не запускает (Bash нет).\n"
|
||||
tools: "Read, Edit, Write, Glob, Grep"
|
||||
model: sonnet
|
||||
color: blue
|
||||
---
|
||||
Ты — помощник по документации проекта **kompas3d-mcp** (MCP-сервер для
|
||||
автоматизации CAD-системы КОМПАС-3D через COM API, .NET 8, C#).
|
||||
|
||||
Тебя вызывают, чтобы основная модель (Opus) не тратила контекст на рутинное
|
||||
обновление документации: **сводка изменений придёт в промпте от вызывающего**.
|
||||
Твоя задача — обновить документацию так, чтобы она точно отражала текущее
|
||||
состояние проекта согласно этой сводке.
|
||||
|
||||
## Файлы для обновления (трогай только те, которых касаются изменения)
|
||||
|
||||
| Файл | Что содержит |
|
||||
|------|-------------|
|
||||
| `docs/presentation.html` | **Презентация проекта — синхронизируй ВСЕГДА.** Счётчики (число инструментов, тестов), бейдж статуса, прогресс-бар, панель инструментов, дорожная карта |
|
||||
| `README.md` | Обзор проекта, список возможностей, команды сборки/запуска |
|
||||
| `CLAUDE.md` | Гайд для агента: «Current state», ключевые факты реализации, что не сделано |
|
||||
| `docs/ARCHITECTURE.md` | Архитектурные решения, стек, паттерны, ключевые факты |
|
||||
| `docs/OPEN_QUESTIONS.md` | Нерешённые вопросы и ограничения |
|
||||
|
||||
## Правила
|
||||
|
||||
1. Прочитай каждый файл перед правкой — не угадывай содержимое.
|
||||
2. Обновляй только секции, которые реально затронуты изменениями.
|
||||
Не переписывай то, что остаётся актуальным.
|
||||
3. Если изменение закрывает вопрос из `OPEN_QUESTIONS.md` — удали его
|
||||
или перенеси в раздел «Решено» (если такой есть).
|
||||
4. Сохраняй стиль, структуру и тон существующих документов.
|
||||
5. Будь лаконичен: одно предложение вместо абзаца, если смысл не теряется.
|
||||
6. Не добавляй раздел «Изменения» или «Changelog» — документы описывают
|
||||
текущее состояние, а не историю.
|
||||
7. Счётчики в `presentation.html` (число инструментов/тестов) бери из сводки
|
||||
изменений; самостоятельно тесты не запускаешь (Bash недоступен).
|
||||
|
||||
После правок кратко сообщи, что именно было изменено в каждом файле.
|
||||
|
||||
## Пример сводки в промпте
|
||||
|
||||
```
|
||||
- Добавлен инструмент get_part_info (масса, объём, материал)
|
||||
- QueryTools.cs перенесён в отдельный файл
|
||||
- Закрыт вопрос из OPEN_QUESTIONS про get_part_info
|
||||
```
|
||||
|
||||
Или короче: «обнови доки — добавил get_part_info».
|
||||
@@ -1,46 +0,0 @@
|
||||
name = "docs-maintainer"
|
||||
description = "ОБЯЗАТЕЛЬНО делегируй этому субагенту (Sonnet) ВСЮ синхронизацию документации проекта с кодом — README.md, AGENTS.md, docs/ARCHITECTURE.md, docs/OPEN_QUESTIONS.md, docs/presentation.html — НЕ правь эти файлы сам. Срабатывает ПРОАКТИВНО как обязательный шаг рабочего цикла, а НЕ по явной команде: как только фича реализована и протестирована, ПЕРЕД коммитом доков передай ему сводку изменений (новые/переименованные/удалённые инструменты, классы, факты реализации, закрытые вопросы OPEN_QUESTIONS, новые счётчики инструментов/тестов для presentation.html). Не жди реплики «обнови доки» — делегируй сам после каждой завершённой единицы работы. Триггеры (включая ТВОИ внутренние): «фича готова, надо отразить в доках», «обнови README/ARCHITECTURE/AGENTS.md/presentation», «закрыт вопрос в OPEN_QUESTIONS», «синхронизируй документацию», «задокументируй изменения». Передавай сводку текстом — код/тесты он сам не запускает (Bash нет)."
|
||||
developer_instructions = """
|
||||
Ты — помощник по документации проекта **kompas3d-mcp** (MCP-сервер для
|
||||
автоматизации CAD-системы КОМПАС-3D через COM API, .NET 8, C#).
|
||||
|
||||
Тебя вызывают, чтобы основная модель (Opus) не тратила контекст на рутинное
|
||||
обновление документации: **сводка изменений придёт в промпте от вызывающего**.
|
||||
Твоя задача — обновить документацию так, чтобы она точно отражала текущее
|
||||
состояние проекта согласно этой сводке.
|
||||
|
||||
## Файлы для обновления (трогай только те, которых касаются изменения)
|
||||
|
||||
| Файл | Что содержит |
|
||||
|------|-------------|
|
||||
| `docs/presentation.html` | **Презентация проекта — синхронизируй ВСЕГДА.** Счётчики (число инструментов, тестов), бейдж статуса, прогресс-бар, панель инструментов, дорожная карта |
|
||||
| `README.md` | Обзор проекта, список возможностей, команды сборки/запуска |
|
||||
| `AGENTS.md` | Гайд для агента: «Current state», ключевые факты реализации, что не сделано |
|
||||
| `docs/ARCHITECTURE.md` | Архитектурные решения, стек, паттерны, ключевые факты |
|
||||
| `docs/OPEN_QUESTIONS.md` | Нерешённые вопросы и ограничения |
|
||||
|
||||
## Правила
|
||||
|
||||
1. Прочитай каждый файл перед правкой — не угадывай содержимое.
|
||||
2. Обновляй только секции, которые реально затронуты изменениями.
|
||||
Не переписывай то, что остаётся актуальным.
|
||||
3. Если изменение закрывает вопрос из `OPEN_QUESTIONS.md` — удали его
|
||||
или перенеси в раздел «Решено» (если такой есть).
|
||||
4. Сохраняй стиль, структуру и тон существующих документов.
|
||||
5. Будь лаконичен: одно предложение вместо абзаца, если смысл не теряется.
|
||||
6. Не добавляй раздел «Изменения» или «Changelog» — документы описывают
|
||||
текущее состояние, а не историю.
|
||||
7. Счётчики в `presentation.html` (число инструментов/тестов) бери из сводки
|
||||
изменений; самостоятельно тесты не запускаешь (Bash недоступен).
|
||||
|
||||
После правок кратко сообщи, что именно было изменено в каждом файле.
|
||||
|
||||
## Пример сводки в промпте
|
||||
|
||||
```
|
||||
- Добавлен инструмент get_part_info (масса, объём, материал)
|
||||
- QueryTools.cs перенесён в отдельный файл
|
||||
- Закрыт вопрос из OPEN_QUESTIONS про get_part_info
|
||||
```
|
||||
|
||||
Или короче: «обнови доки — добавил get_part_info»."""
|
||||
@@ -1,72 +0,0 @@
|
||||
name = "kompas-sdk-research"
|
||||
model = "gpt-5.6-terra"
|
||||
model_reasoning_effort = "medium"
|
||||
sandbox_mode = "read-only"
|
||||
description = "ОБЯЗАТЕЛЬНО делегируй этому read-only субагенту ЛЮБОЙ поиск по справке COM API КОМПАС — она живёт в RAG-базе kompas-sdk (MCP), а не в репозитории. Срабатывает ПРОАКТИВНО, без явной просьбы пользователя: как только при планировании или реализации фичи понадобилась сигнатура метода (Automation/COM), значения перечисления (Obj3dType, ksHoleTypeEnum, ST_MIX_*, стили линий…), нужный интерфейс под задачу, единицы измерения или цепочка вызовов — СНАЧАЛА делегируй исследование этому агенту, ПОТОМ пиши код. Агент возвращает сжатую выжимку, а основная задача при необходимости перепроверяет спорные детали рефлексией по libs/kompas-interop/*.dll. Триггеры: «нужна сигнатура …», «какие значения enum …», «каким интерфейсом сделать X через API КОМПАС», «что возвращает <метод>», «как через COM API построить …», «найди в справке КОМПАС». НЕ для построения геометрии в КОМПАС и НЕ для KsAPI (Qt/C++ — в базе его нет)."
|
||||
developer_instructions = '''
|
||||
Ты — помощник по навигации в справке **КОМПАС SDK** для проекта
|
||||
**kompas3d-mcp** (MCP-сервер для КОМПАС-3D через COM API, .NET 8, C#).
|
||||
|
||||
Справка — это RAG-база **`kompas-sdk`** (2465 статей из репозитория
|
||||
`kompas-sdk-docs`, версия SDK v24), доступная через MCP-инструменты
|
||||
`search_knowledge`, `grep_knowledge`, `get_document`, `get_chunk_context`,
|
||||
`knowledge_status`. Статьи — по интерфейсам, перечислениям, структурам и
|
||||
руководствам, с полями `title`, `type`, `api`, `domain`, `tags`; методы и
|
||||
свойства — секции `## Имя - Описание` внутри статьи интерфейса.
|
||||
**В репозитории проекта справки больше нет** — grep по нему её не найдёт.
|
||||
|
||||
Тебя вызывают, чтобы основная задача не тратила контекст на поиск:
|
||||
**вопрос для исследования придёт в промпте от вызывающего**. Найди ответ через
|
||||
MCP и верни сжатую выжимку (интерфейс, сигнатура, параметры, значения констант,
|
||||
источник).
|
||||
|
||||
## Когда это про тебя
|
||||
|
||||
- Нужна сигнатура метода (`SetSideParam`, `GetGabarit`, …) — Automation/COM, параметры, возврат.
|
||||
- Нужны значения перечисления/констант (`Obj3dType`, `ksMassUnitsEnum`, `ST_MIX_*`, стили линий).
|
||||
- Нужно найти интерфейс под задачу («чем построить массив», «как задать переменную»).
|
||||
- Нужен паттерн «как через API сделать X» с цепочкой вызовов.
|
||||
|
||||
## Порядок поиска
|
||||
|
||||
1. **Точное имя** — `grep_knowledge("SetSideParam")`, `grep_knowledge("ksHoleTypeEnum")`,
|
||||
`grep_knowledge("o3d_incline")`. Имена методов — заголовки `## SetSideParam - …`,
|
||||
поэтому поиск сразу ведёт в нужную статью; регулярки работают.
|
||||
2. **Задачный вопрос** — `search_knowledge` с фильтрами `type`
|
||||
(`interface`/`enum`/`struct`/`guide`), `tags` (`api7`, `api5`, `3d`, `2d`,
|
||||
`sketch`, `assembly`, `drawing`, `feature`), `path_prefix` (`sdk/enums/`).
|
||||
3. **Окрестность найденного** — `get_chunk_context(document_id, chunk_index, window=2)`.
|
||||
4. `get_document` — только для небольших статей: `sdk/interfaces/геометрия.md`
|
||||
весит 261 КБ и выгрузкой целиком сожжёт контекст.
|
||||
5. `knowledge_status` — если выдача подозрительно пуста (покажет коммит и объём индекса).
|
||||
|
||||
Точные числовые значения констант сверяются в заголовках SDK на машине
|
||||
разработчика: `C:\\Program Files\\ASCON\\KOMPAS-3D v24 Home\\SDK\\Include\\*.h`
|
||||
(`ksConstants*.h`, `ldefin2d.h`) — у тебя их нет, просто укажи, где смотреть.
|
||||
|
||||
## Важно — что в базе и чего нет
|
||||
|
||||
- База содержит **COM API7/API5** — то, что использует проект. Это твой основной источник.
|
||||
- Кроссплатформенный Qt/C++ **KsAPI** (`ksapi_*`) в базу **не включён** — если задача
|
||||
явно про KsAPI, скажи об этом (нужен отдельный источник).
|
||||
|
||||
## Правила
|
||||
|
||||
1. Не угадывай — находи через MCP-инструменты и читай первоисточник.
|
||||
2. Верни **сжатую выжимку**, а не дамп статьи:
|
||||
- имя интерфейса и метода/свойства;
|
||||
- синтаксис Automation (и COM, если важен порядок out-параметров);
|
||||
- параметры (имя, тип, смысл) и возвращаемое значение;
|
||||
- для enum/констант — пары «имя = значение» и их смысл;
|
||||
- важные примечания (единицы измерения, побочные эффекты, версия КОМПАС);
|
||||
- `path` статьи (например `sdk/interfaces/kspart.md`) как ссылку на источник.
|
||||
3. Если у метода несколько перегрузок или связанных интерфейсов — перечисли релевантные.
|
||||
4. Если в COM-interop имя отличается от справки (частый случай в этом проекте), отметь это
|
||||
как вероятную тонкость и предложи свериться рефлексией по `libs/kompas-interop/*.dll`.
|
||||
5. Если MCP не отвечает — так и скажи, не выдумывай сигнатуры: локальной копии справки нет.
|
||||
6. Только исследование и ответ — правок в код или документацию не делаешь.
|
||||
|
||||
## Пример задачи в промпте
|
||||
|
||||
«Нужна сигнатура и единицы измерения у метода расчёта МЦХ детали через API5 — что
|
||||
возвращает, как задать мм/кг.» Или короче: «как построить массив по сетке (pattern)».'''
|
||||
@@ -1,2 +0,0 @@
|
||||
[mcp_servers.kompas]
|
||||
command = 'E:\AiProjects\kompas3d-mcp\src\Kompas.Mcp.Host\bin\Release\net8.0-windows\win-x64\kompas-mcp.exe'
|
||||
@@ -1,100 +0,0 @@
|
||||
---
|
||||
description: "Исследование COM API КОМПАС-3D по MD-базе docs/Kompas3D_SDK/. Вызывай для любой сигнатуры метода, значения enum, цепочки вызовов COM API — прежде чем писать код. НЕ для построения геометрии (это MCP-инструменты) и НЕ для KsAPI (Qt/C++)."
|
||||
mode: subagent
|
||||
model: ct114/ornith-1.0-35b
|
||||
additional:
|
||||
variant: fast
|
||||
permission:
|
||||
read: allow
|
||||
edit: deny
|
||||
glob: allow
|
||||
grep: allow
|
||||
list: allow
|
||||
bash: deny
|
||||
task: deny
|
||||
webfetch: deny
|
||||
skill: deny
|
||||
question: deny
|
||||
todowrite: deny
|
||||
---
|
||||
|
||||
Ты — помощник по навигации в справке **КОМПАС SDK** в репозитории
|
||||
**kompas3d-mcp** (MCP-сервер для КОМПАС-3D через COM API, .NET 8, C#).
|
||||
Справка — это MD-база знаний `docs/Kompas3D_SDK/`: статьи по интерфейсам с
|
||||
YAML-фронтматтером (`type`, `api`, `domain`, `tags`, `sources`); методы и
|
||||
свойства — заголовки `## ` внутри статьи интерфейса.
|
||||
|
||||
Тебя вызывают, чтобы основная модель не тратила контекст на поиск:
|
||||
**вопрос для исследования придёт в промпте от вызывающего**. Прогони Grep/Read
|
||||
по базе, прочитай нужную статью и верни сжатый ответ (интерфейс, сигнатура,
|
||||
параметры, значения констант, источник).
|
||||
|
||||
## Когда это про тебя
|
||||
|
||||
- Нужна сигнатура метода (`SetSideParam`, `GetGabarit`, …) — Automation/COM, параметры, возврат.
|
||||
- Нужны значения перечисления/констант (`Obj3dType`, `ksHoleTypeEnum`, `ST_MIX_*`, стили линий).
|
||||
- Нужно найти интерфейс под задачу («чем/каким интерфейсом построить X»).
|
||||
- Нужен паттерн «как через API сделать X» с цепочкой вызовов.
|
||||
|
||||
## Структура MD-базы `docs/Kompas3D_SDK/`
|
||||
|
||||
```
|
||||
index.md — оглавление по 4 категориям со ссылками на статьи
|
||||
interfaces/*.md — одна статья на интерфейс/тему; все методы и свойства как ## секции
|
||||
enums/*.md — перечисления (пары «имя | значение | смысл»)
|
||||
structures/*.md — структуры и параметрические интерфейсы
|
||||
guides/*.md — концепции и руководства
|
||||
resources/ — изображения (ссылки в статьях вида ../resources/имя.png)
|
||||
```
|
||||
|
||||
Каждая статья начинается с YAML-фронтматтера: `type` (interface/enum/struct/guide),
|
||||
`api` (api7/api5), `domain` (3d/2d/sketch/assembly/…), `tags`, `sources` (исходные `*.html`).
|
||||
Имя файла статьи — имя интерфейса (`iapplication.md`, `ksbossextrusiondefinition.md`)
|
||||
или читаемый слаг темы (`приложение.md`). Методы/свойства внутри — заголовки `## Имя - Описание`.
|
||||
|
||||
> База **хранится в репозитории** (канонический источник справки) — отдельная
|
||||
> генерация не нужна.
|
||||
|
||||
## Инструменты (используй именно их, не веб)
|
||||
|
||||
- **Grep** по `docs/Kompas3D_SDK/` — ищи имя интерфейса/метода/свойства, русское
|
||||
ключевое слово или тег. Имена методов — это заголовки `## SetSideParam - …`,
|
||||
поэтому поиск по имени метода сразу ведёт в нужную статью.
|
||||
- **Read** найденного `*.md` — статья интерфейса содержит все его методы/свойства,
|
||||
блоки `Синтаксис Automation:`/`Синтаксис COM:` (в code-fence), параметры, примечания.
|
||||
- `docs/Kompas3D_SDK/index.md` — оглавление по категориям, когда нужно осмотреть тему.
|
||||
|
||||
Полезные приёмы поиска:
|
||||
- по перечислению: `Grep "ksMassUnitsEnum"` или загляни в `enums/`;
|
||||
- фильтр по тегам в фронтматтере: `Grep "domain: \[.*3d"` , `Grep "type: enum"`;
|
||||
- веер по многим интерфейсам сразу: `Grep "IPart7|IDocument3D|IPart"` по `interfaces/`.
|
||||
|
||||
Точные числовые значения констант при необходимости сверяй в заголовках SDK:
|
||||
`C:\Program Files\ASCON\KOMPAS-3D v24 Home\SDK\Include\*.h` (например `ksConstants*.h`, `ldefin2d.h`).
|
||||
|
||||
## Важно — что в базе и чего нет
|
||||
|
||||
- База содержит **COM API7/API5** — то, что использует проект (файлы `iapplication*`,
|
||||
`kspart*`, `ksmassinertiaparam*` и т.п.). Это твой основной источник.
|
||||
- Кроссплатформенный Qt/C++ **KsAPI** (`ksapi_*`) в базу **не включён** — если задача
|
||||
явно про KsAPI, скажи об этом (нужен отдельный источник).
|
||||
|
||||
## Правила
|
||||
|
||||
1. Не угадывай — находи через Grep/Read по `docs/Kompas3D_SDK/` и читай первоисточник.
|
||||
2. Верни **сжатую выжимку**, а не дамп статьи:
|
||||
- имя интерфейса и метода/свойства;
|
||||
- синтаксис Automation (и COM, если важен порядок out-параметров);
|
||||
- параметры (имя, тип, смысл) и возвращаемое значение;
|
||||
- для enum/констант — пары «имя = значение» и их смысл;
|
||||
- важные примечания (единицы измерения, побочные эффекты, версия КОМПАС);
|
||||
- имя статьи / `sources` из фронтматтера как ссылку на источник.
|
||||
3. Если у метода несколько перегрузок или связанных интерфейсов — перечисли релевантные.
|
||||
4. Если в COM-interop имя отличается от справки (частый случай в этом проекте), отметь это
|
||||
как вероятную тонкость и предложи свериться рефлексией по `libs/kompas-interop/*.dll`.
|
||||
5. Только исследование и ответ — правок в код или документацию не делаешь.
|
||||
|
||||
## Пример задачи в промпте
|
||||
|
||||
«Нужна сигнатура и единицы измерения у метода расчёта МЦХ детали через API5 — что
|
||||
возвращает, как задать мм/кг.» Или короче: «как построить массив по сетке (pattern)».
|
||||
@@ -1,138 +0,0 @@
|
||||
# AGENTS.md
|
||||
|
||||
This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
|
||||
|
||||
## Project goal
|
||||
|
||||
Build an **MCP (Model Context Protocol) server** that lets an LLM drive the CAD system **КОМПАС-3D** (ASCON). The server exposes КОМПАС operations (create/open documents, build 2D geometry, 3D parts and assemblies, read/write parameters, run macros) as MCP tools.
|
||||
|
||||
Authoritative references:
|
||||
- **SDK knowledge base in this repo: `docs/Kompas3D_SDK/`** — clean Markdown, one article per COM interface, with YAML tags; search it with Grep/Read (see below). Committed to the repo (canonical), distilled from the КОМПАС SDK help. Prefer this over the web. For non-trivial lookups, dispatch the **`kompas-sdk-research`** subagent (Haiku) to save context.
|
||||
- Online SDK docs: https://help.ascon.ru/KOMPAS_SDK/22/ru-RU/index.html
|
||||
- Local SDK install: `C:\Program Files\ASCON\KOMPAS-3D v24 Home\SDK`
|
||||
|
||||
## Navigating the SDK docs (`docs/Kompas3D_SDK/`)
|
||||
|
||||
`docs/Kompas3D_SDK/` is a structured MD knowledge base distilled from the 26 000-page Help&Manual WebHelp export. One article per COM interface/topic, split into `interfaces/`, `enums/`, `structures/`, `guides/` (+ `resources/` images), each with YAML frontmatter (`type`, `api`, `domain`, `tags`, `sources`); methods/properties are `## ` headings inside the interface article. **Search it directly — do not browse the raw HTML:**
|
||||
|
||||
- **Grep** over `docs/Kompas3D_SDK/` for an interface/method/property name or Russian keyword (method names are `## SetSideParam - …` headings, so the name lands you in the right article).
|
||||
- **Read** the matching `*.md` — the interface article holds all its members, `Синтаксис Automation:`/`Синтаксис COM:` blocks (in code fences), parameters, notes.
|
||||
- `docs/Kompas3D_SDK/index.md` — table of contents by category. Filter by frontmatter tags, e.g. `Grep "type: enum"` or `Grep "domain: \[.*3d"`.
|
||||
|
||||
The base is committed to the repo (canonical) — no regeneration step. It covers the **COM API7/API5** reference this project uses (interfaces named `iapplication_*`, `idocuments_*`, …). The cross-platform Qt/C++ **KsAPI** flavour (`ksapi_*`, non-COM binding) is intentionally **excluded** from the base — ignore unless explicitly working with KsAPI.
|
||||
|
||||
## Current state
|
||||
|
||||
**Status:** v1 + v2 + STEP/assembly + direct B-rep edit + structural inspection + 2D drawings — implemented and working (full sketch→feature→inspect→STEP round-trip + `move_face` + `describe_model` validated end-to-end).
|
||||
Stack: **.NET 8 (`net8.0-windows`, x64), C#**, MCP via the official `ModelContextProtocol` SDK over **stdio**. **83 MCP tools, 331 tests green (201 unit + 130 integration).**
|
||||
|
||||
Where to look (single source of truth — do **not** duplicate these lists here):
|
||||
- **Full tool catalog** (by group, all 83) → [`README.md`](README.md) §«Инструменты».
|
||||
- **Roadmap / what's already done** → [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) §10.
|
||||
- **Backlog / what's left** → [`docs/TODO.md`](docs/TODO.md) (canonical; top of «2D-ЧЕРТЁЖ» = next priority. Associative diametral/radial→circle binding (`associate` flag), sheet format (`drawing_set_sheet_format`), leaders (`drawing_add_leader`) done — next: bases, tolerances, arc/angular/rough binding; plus extra mate types, package E continuation).
|
||||
- **Design decisions / caveats** → [`docs/OPEN_QUESTIONS.md`](docs/OPEN_QUESTIONS.md). Known caveat («Ревью v2»): boss/cut direction on a selected face depends on face-normal orientation — `forward` may need flipping; the agent picks via snapshot or `describe_face` normal.
|
||||
- **Verified COM call-chains & gotchas** → «Key implementation facts» below + memory files (`kompas-*-api*.md`).
|
||||
|
||||
Principle: **MCP = translating SDK capabilities into general tools** (not task-specific). On top of MCP — skill **`.Codex/skills/kompas-3d/`** with a methodology (playbooks, heuristics, validated in `usecases/`). Layout: `usecases/` (in .gitignore) is the proving ground; proven techniques are promoted to the skill. A separate skill **`.Codex/skills/kompas-fdm-design/`** is a DFM methodology layer for FDM/FFF 3D-printing (overhang rules, wall thickness, teardrop holes, fits/clearances, orientation for strength, elephant foot, bosses/inserts/snap-fits) with a lightweight geometry audit using existing inspection tools; it works on top of `kompas-3d` (the build layer) and exports via `export_step` — no slicer integration.
|
||||
|
||||
**Delegation subagents** (`.Codex/agents/`, dispatched via the `Agent` tool to keep heavy work off Opus's context — system prompt lives in the agent definition, so pass only the variable part as the prompt):
|
||||
- **`kompas-sdk-research`** (model: **Haiku**, read-only — `Glob`/`Grep`/`Read`): finds an interface/method/enum/constant signature in the `docs/Kompas3D_SDK/` MD knowledge base and returns a compressed summary. Dispatch it for any non-trivial SDK lookup instead of grepping the help yourself; a wrong answer is cheap — Opus re-verifies against `libs/kompas-interop/*.dll` by reflection.
|
||||
- **`docs-maintainer`** (model: **Sonnet**, `Read`/`Edit`/`Write`/`Glob`/`Grep`): syncs `README.md`/`AGENTS.md`/`docs/ARCHITECTURE.md`/`docs/OPEN_QUESTIONS.md`/`docs/presentation.html` with code changes from a change summary you provide. Dispatch it for doc updates instead of editing docs by hand. (Both were skills before — converted to native subagents; the model split reflects task type: bounded retrieval → Haiku, judgement-heavy editing → Sonnet.)
|
||||
|
||||
Layout: `src/Kompas.Mcp.Core` (COM layer), `src/Kompas.Mcp.Host` (MCP stdio server + tools),
|
||||
`tests/Kompas.Mcp.Tests` (unit + integration), `libs/kompas-interop/*.dll` (vendored КОМПАС interop
|
||||
assemblies from SDK `Samples/Common`, referenced via `Directory.Build.props` → `KompasInteropDir`).
|
||||
|
||||
```powershell
|
||||
dotnet build -c Release # build
|
||||
dotnet test --filter "Category=Unit" # unit tests (no COM)
|
||||
dotnet test --filter "Category=Integration" # integration (requires running КОМПАС)
|
||||
dotnet run --project src/Kompas.Mcp.Host # start the MCP server (stdio)
|
||||
```
|
||||
|
||||
## Key implementation facts (don't relearn)
|
||||
|
||||
- **All COM calls run on one dedicated STA thread** (`KompasDispatcher.InvokeAsync`); КОМПАС is single-instance STA. Services never touch COM off that thread.
|
||||
- **Connection**: API5 (`KOMPAS.Application.5` → `KompasObject`) then `ksGetApplication7()` → `IApplication`; the API5 root is needed for `ksPart` 3D-building and `ksDocument3D` snapshots. API7 is used for app/documents, holes, assemblies, drawings, direct B-rep edit.
|
||||
- **3D via API5 `ksPart`**: `NewEntity(o3d_*)` + matching definition; always check `entity.Create()` return. Entity codes: `o3d_sketch`/`o3d_bossExtrusion`/`o3d_cutExtrusion`/`o3d_bossRotated`/`o3d_fillet`/`o3d_chamfer`, `o3d_shellOperation=43`, `o3d_ribOperation=44`, `o3d_baseEvolution=45` (sweep), `o3d_baseLoft=30`, `o3d_planeOffset=14`, `o3d_meshCopy=35` (linear pattern), `o3d_circularCopy=36`, `o3d_mirrorOperation=48`, `o3d_mirrorAllOperation=49`, `o3d_incline=42` (draft), axes `o3d_axisOX/OY/OZ=71/72/73`. Definitions: `ksBossExtrusionDefinition`/`ksCutExtrusionDefinition`/`ksBossRotatedDefinition`/`ksFilletDefinition`/`ksChamferDefinition`/`ksShellDefinition`/`ksRibDefinition`/`ksBaseEvolutionDefinition`/`ksBaseLoftDefinition`/`ksPlaneOffsetDefinition`/`ksMeshCopyDefinition`/`ksCircularCopyDefinition`/`ksMirrorCopyDefinition`/`ksMirrorCopyAllDefinition`/`ksInclineDefinition`. Cut-through holes use `dtBoth` + `etThroughAll`.
|
||||
- **Face/edge selection**: `ksPart.EntityCollection(o3d_face|o3d_edge)` → `SelectByPoint(x,y,z)` (world mm) → `GetByIndex(0)`; or by stable index from `list_faces`/`list_edges` (helpers `SelectFaceByIndex`/`SelectEdgeByIndex`, range-checked before mutation; by-index more reliable than by-point). Classify: `ksFaceDefinition.IsPlanar/IsCylinder/…` + `GetArea(ST_MIX_MM)`; `ksEdgeDefinition.IsLineSeg/IsCircle/IsArc/…` + `GetLength()`. Sketch axis = line with system style **3** (осевая).
|
||||
- **МЦХ / mass props**: use **API5 `ksPart.CalcMassInertiaProperties(ST_MIX_MM|ST_MIX_KG)`** → `ksMassInertiaParam` (`v`/`m`/`F`/`xc`/`yc`/`zc`), computed on demand. **Do NOT** use API7 `IMassInertiaParam7.Calculate()` — its `Actual` flag sticks after the first calc and won't refresh on API5 geometry changes (stale volume). Bounding box: `ksPart.GetGabarit(full:false, …)`.
|
||||
- **Snapshot**: `ksDocument3D.SaveAsToRasterFormat(file, ksRasterFormatParam)` renders to a temp file (the in-memory `resultArrayBytes` stays empty when a filename is given); read bytes back. Return to MCP via `ImageContentBlock.FromBytes(bytes, mime)` — **not** `Data = bytes` (Data holds base64 bytes). Use `model_snapshot` only for visually-spatial questions; **prefer `describe_model` first** (structural «passport» — bbox + МЦХ + bodies + topology + feature tree + variables, no image token cost).
|
||||
- Logs go to **stderr** (stdout is the MCP channel). Integration tests reuse one КОМПАС via `KompasFixture`; artifacts land in gitignored `.scratch/`. All integration test classes inherit `IntegrationTestBase` (`IAsyncLifetime`) → `DocumentService.CloseAllAsync` after each test (prevents tab accumulation).
|
||||
- **Feature tree API**: read via API5 `ksPart.GetFeature()` → `(ksFeature).SubFeatureCollection(true,false)` → `ksFeatureCollection`. Cast `(ksFeature)part` does NOT work (RCW QI returns null) — must use `GetFeature()`. `ksFeature.type` returns only coarse `o3d_entity` and does NOT distinguish operations; precise type and params come from `ksFeature.GetObject()` → `ksEntity.GetDefinition()` + the matching definition (`ksBossExtrusionDefinition.GetSideParam`, `ksFilletDefinition.radius`, `ksChamferDefinition.GetChamferParam`, …). Node names ("Элемент выдавливания:1") are localized by КОМПАС. Features registered by id in `_features` support patterns/mirror; helpers `RequireSketch`/`RequireFeature`.
|
||||
- **Variables (API5)**: create/read via `ksPart.GetFeature().VariableCollection` (property on root ksFeature) — **NOT** `ksPart.VariableCollection()` (returns external-only). Expression is the leading field; value is derived. Apply via `ksPart.RebuildModel()` (check return); after rebuild the RCW variable is stale → re-read via a fresh collection (`ReadValueFresh`). Dependents recalculate automatically; deletion fails if dependents exist (`RemoveVariable` returns FALSE). **Geometry limitation:** variables drive geometry only in a parametric model (sketch dims linked to variable names); our sketches use literal coords → `set_variable` stores/computes the value but does NOT move geometry. Parametric sketches **unavailable via COM** (`ksCDimWithVariable` not constructible from external automation — see `docs/superpowers/specs/2026-05-27-parametric-sketch-findings.md`; do not retry without new info).
|
||||
- **Topology / measure API**: `ksFaceDefinition.EdgeCollection` / `GetCylinderParam` / `GetSurface().GetNormal` + `normalOrientation`; `ksEdgeDefinition.GetAdjacentFace(bool)` / `GetVertex(bool)` → `ksVertexDefinition.GetPoint`; bodies via `ksPart.BodyCollection()` → `ksBody` (`IsSolid` / `FaceCollection`); measurements via `ksPart.GetMeasurer()` → `ksMeasurer` (`SetObject1/2`, `unit=ST_MIX_MM`, `Calc`, `distance`/`MinDistance`/`angle` in degrees / `IsAngleValid`).
|
||||
- **"STEP import without history"** detected structurally: `bodyCount > 0 && no formative features && features.Count <= bodyCount+1` (only origin + body). STEP with edits ("Смещённая плоскость" / "Разрезать" / "Переместить грани" / "Булева операция") already has history.
|
||||
- **STEP import/export COM pattern** (verified): `IApplication.get_Converter((object)(int)ksConverterFromSTEP=-3)` (pass format code, not DLL path) → `IConverter.ConverterParameters(cmd)` → `IAdditionConvertParameters.Format=ksConverterFromSTEP` → `IKompasDocument3D1.ConvertFromAdditionFormat(path, prm)`. Export: `ConvertToAdditionFormat` (format codes AP203/AP214/AP242). Param co-classes (AdditionConvertParameters etc.) are NOT CoCreatable — only via converter factories. Assembly traversal (`list_components`): `IKompasDocument3D.TopPart → IPart7.Parts (IParts7)`. Extract component: `prm.NeedCreateComponentsFiles=true` (writes .m3d files next to STEP) + `IPart7.OpenSourceDocument`.
|
||||
- **Direct B-rep face editing (`move_face`)** (verified): API7 face via `IPart7.FindObjectsByPoint(x,y,z,true)` → cast to `KompasAPI7.IFace`; containers via runtime COM-QI `(ISurfaceContainer)part`, `(IModelContainer)part`; `FaceMover` (SetFaces + Offset + Direction + Update) moves the face. distance>0 = outward (add material), <0 = inward. Works on parametric and imported B-rep. Full split-reposition workflow (SplitSolids/BodyRepositions) is driveable; only partly exposed as tools (`split_solid_by_plane`, `move_body`, `boolean_union`).
|
||||
- **Forming ops (API5 definitions)**:
|
||||
- `shell` (`ksShellDefinition`, o3d_shellOperation=43): `thickness` + `thinType` (bool, `= !outward`) + `FaceArray()` (open faces ≥1 by index). Transient RCWs intentionally not released (consistent with Fillet/Chamfer).
|
||||
- `rib` (`ksRibDefinition`, =44): `SetSketch(sketch)` → `index=0`, `angle`, `side` (`left|right|up|down` → 0/1/2/3 via `SketchGeometry.RibSide()`); `SetThinParam(dtBoth, t/2, t/2)` if symmetric, else `(dtNormal, t, 0)`. Empirical: the rib contour must float in the gap (not touch the body) — КОМПАС extends the web automatically.
|
||||
- `sweep` (`ksBaseEvolutionDefinition`, =45): `SetSketch(profile)` → `PathPartArray().Add(path)` → `sketchShiftType=0` → `SetThinParam(false)` → `Create()`. Profile & path on different (usually ⟂) planes; `profileSketchId != pathSketchId`.
|
||||
- `loft` (`ksBaseLoftDefinition`, =30): `Sketchs().Add(each)` → `SetLoftParam(false,false,true)` → `SetThinParam(false)`. ≥2 closed sections on parallel planes — use `sketch_create_on_offset_plane` for non-base sections.
|
||||
- offset plane (`sketch_create_on_offset_plane`, `ksPlaneOffsetDefinition`, o3d_planeOffset=14): `SetPlane(base)` + `offset` mm + `direction` → `Create()` → sketch on it (`OpenSketchOnOffsetPlaneAsync`).
|
||||
- `draft` (`ksInclineDefinition`, o3d_incline=42 — API5 calls the op **Incline**, NOT Draft): `FaceArray()` (by index) + `SetPlane(neutral base plane)` + `angle` (degrees) + `direction`. **Empirical, opposite to SDK docs:** `direction=false`=expanding/outward (adds material), `true`=tapering/inward → code maps `def.direction = !outward`. Validate `0<angle<90`. (`o3d_DraftFromEdges=644` / `IDraftFromEdges` is a different op — not used.)
|
||||
- **Patterns & mirror (API5)**: `linear_pattern` (`ksMeshCopyDefinition`, =35): `SetAxis1(axis)` + `SetCopyParamAlongAxis(true,0,count,step,false)` + `count2=1` (disables 2nd direction → 1D); features via `OperationArray()`. `circular_pattern` (`ksCircularCopyDefinition`, =36): `SetAxis(axis)`; `count1=1` (radial off), `count2=count` (ring), `step2=angle°` between neighbours, `factor2=false`, `inverce=reverse`; `GetOperationArray().Add(feature)`. `mirror_operation` (`ksMirrorCopyDefinition`, =48): `SetPlane(base)` + `GetOperationArray().Add(feature)`. `mirror_body` (`ksMirrorCopyAllDefinition`, =49): `SetPlane(base)`; `ChooseBodies()` casts to null in interop and is NOT needed — mirrors all bodies keeping the original (volume doubles). Empirical: `count` includes the original instance (count=3 → 3 instances). Axes: `CoordinateAxis{X,Y,Z}` → `o3d_axisOX/OY/OZ` (`Core/Modeling/CoordinateAxis.cs`, analogous to `BasePlane.cs`).
|
||||
- **Hole ops (API7 only — `ksHoleDefinition` absent in API5 interop; `HoleService`)**: `HoleCore(ksHoleTypeEnum holeType, Action<IHole3D> configure)` — `configure` sets `HoleParameters` **after** `HoleType` is assigned (the type-specific sub-interface is inaccessible until the type is fixed). Placement shared: `IModelContainer`/`IHole3D`/`IHoleDisposal` + `Points3D`, direction chosen by volume delta, orphan rollback. None registered in `_features` → no id returned. `hole`=`ksHTBase`, configure=null. `hole_counterbore`=`ksHTCounterbore` + `(ISpotfacingHoleParameters)` (`SpotfacingDiameter`/`SpotfacingDepth`). `hole_countersink`=`ksHTCountersinking` + `(ICountersinkHoleParameters)` (`CountersinkType=ksCTDiameterAngle`, `CountersinkDiameter`/`Angle`). `hole_conic`=`ksHTConic` + `(IConicHoleParameters)` (`ConicType=ksCNAngle`, `ConicAngle`).
|
||||
- **Assembly (API7, `AssemblyService`, namespace `Kompas.Mcp.Core.Assemblies`)**: `assembly_add_component` — `IParts7.AddFromFile(path, ExternalFile=true, Redraw=true)` → `IPart7.Placement` (`IPlacement3D`) → `SetOrigin(x,y,z)` (mm, assembly world CS) → `UpdatePlacement(true)` → `top.RebuildModel(true)`. **Empirical:** `UpdatePlacement` returns FALSE for a manually-positioned component (no mates) — NOT an error. Validate `DocumentType==ksDocumentAssembly` before touching `TopPart`; rollback via `IFeature7.Delete` + `RebuildModel`. `assembly_add_mate` — `top.MateConstraints` (`IMateConstraints3D`) → `Add(MateConstraintType)` → `BaseObject1/2` (faces via `top.FindObjectsByPoint(x,y,z,FirstLevel=false)`) → `ParamValue` (distance) → `Update()` → `RebuildModel(true)`; check `mate.Valid` (false → rollback `mate.Owner.Delete`). Types `coincidence`(mc_Coincidence=0) / `distance`(mc_Distance=5, value>0) verified live; `parallel`/`perpendicular`/`concentric`/`angle`/`tangency` — future.
|
||||
- **Drawing (API7, `DrawingService`, namespace `Kompas.Mcp.Core.Drawings`)** — entry via `IKompasDocument2D`; requires an active drawing doc (`DocumentType==ksDocumentDrawing`). All coords are **view-local** (mm) unless noted. Model file must be saved before placing views.
|
||||
- `drawing_create_standard_views`: `doc2d.ViewsAndLayersManager.Views.AddStandartViews(path, "#Спереди", projTypes, x, y, scale, dx=20, dy=20)` where `projTypes=object[]{1,3,5}` (Front/Up/Left as SAFEARRAY VT_I4). Success: `created==3`; content check: sum of new views' `IView.ObjectCount` > 0 else rollback `IView.Delete`. Returns `ViewNumbers` (the `IView.Number` values — address views by these when placing annotations).
|
||||
- `drawing_fill_title_block`: `doc2d.LayoutSheets.ItemByNumber[1].Stamp` (`IStamp`) → `stamp.Text[columnId].Str = text` (indexed property; `IText.Str` overwrites, no Clear needed) → `stamp.Update()`. Fields→columns (`ksStampEnum`): Name=1, Designation=2, Material=3. Non-transactional (all cells → single `Update`).
|
||||
- **Dimensions go into `(ISymbols2DContainer)view`, NOT the sheet, and are NOT counted in `IView.ObjectCount`** (that counts geometry in `IDrawingContainer`) — verify placement via the container's `*.Count`. Common flow: `Add()` → set coords → `AutoNominalValue=true` → `Update()` → check `Valid` → read `((IDimensionText)dim).NominalValue`; rollback `dim.Delete()` on FALSE/invalid/zero. Leader/angle in **radians** (`DimensionAngles.ToRadians`). Free placement = set coords directly; **associative** (diametral/radial only) = set `BaseObject` to a projected circle → value read from geometry (see below).
|
||||
- linear (`ILineDimension`): `X1,Y1,X2,Y2` (extension pts) + `X3,Y3` (dim line) + `Orientation` (`DimensionOrientation{Horizontal,Vertical,Parallel}` → `ksLinDParallel=0`/`Horizontal=1`/`Vertical=2`; parallel needs no explicit Angle).
|
||||
- diametral (`IDiametralDimension`): `Xc,Yc,Radius` + `Angle`; `NominalValue` = 2·Radius.
|
||||
- radial (`IRadialDimension`): `Xc,Yc,Radius` + `Angle`, `DimensionType=true`; **`NominalValue` = Radius, NOT diameter** — contrary to SDK docs, confirmed by spike.
|
||||
- **associative** diametral/radial (`associate=true` flag): instead of coords, find a projected circle in `(IDrawingContainer)view.Circles` by center+radius key (`CircularObjectMatch.SelectMatchIndex`, tol 1 mm; throws on none/ambiguous; concentric disambiguated by radius), set `dim.BaseObject = circle` (`_Circle` implements `IDrawingObject`) → `NominalValue` read from geometry. `RequireViewContainers` gives both QIs; free path stays on `RequireSymbols2DContainer`. Arc/angular/rough binding — not yet (own spike needed; `ILineDimension` has no `BaseObject`).
|
||||
- angular (`IAngleDimension`, `AngleDimensions.Add(ksDrADimension=10)`): vertex `Xc,Yc` + side pts `X1,Y1`/`X2,Y2` + arc-position `X3,Y3` (positions the arc only, does NOT pick the angle); measured angle chosen by `DimensionType` = `AngleDimensionType{Min,Max,More}` (on rays 0°/45° → min=45°/max=135° supplement/more=315° reflex). `NominalValue` in degrees.
|
||||
- `drawing_add_rough` (`Roughs.Add()`/`IRough`: `BranchX0/Y0`, `Angle`) + `(IRoughParams)rough` QI: `SignType` = `RoughSignType{NoProcessing,DeleteMaterial,WithoutDeleteMaterial}`, value (Ra/Rz) = `RoughParamText.Str`.
|
||||
- `drawing_add_text` — **text lives in `IDrawingContainer`, NOT `ISymbols2DContainer`, and NOT in `IView.ObjectCount`**: `(IDrawingContainer)view.DrawingTexts.Add()`/`IDrawingText` (`X/Y/Angle`) + `(IText)dt.Str` = text; count via `DrawingTexts.Count`. Do NOT set `Height` (that's block height, not font).
|
||||
- `drawing_set_technical_requirements` — **document-level**: `(IDrawingDocument)doc.TechnicalDemand`/`ITechnicalDemand`: `td.Text.Str = text` → `td.Update()` (`IsCreated` False→True on first set; overwrites; `\n` multiline).
|
||||
- `DrawingAnnotationResult{Value(read-back), ViewNumber}` is the shared result for rough/text.
|
||||
- `drawing_add_leader` — `ISymbols2DContainer.Leaders.Add(ksDrLeader)` → `IBaseLeader`. **КРИТИЧЕСКИЙ ПОРЯДОК (спайк):** новая выноска имеет 0 ответвлений → `(IBranchs)bl.AddBranchByPoint(0,x,y)` (остриё) ДО `SetBranchTextPosition(textX,textY)`/`Update`, иначе КОМПАС падает `RPC_E_SERVERFAULT`. Текст — `(ILeader)bl.TextOnShelf.Str`; `ShelfDirection` (enum `ShelfDirection{Auto,Right,Left,Up,Down}`, Auto = не задавать). `AddBranchByPoint`/`SetBranchTextPosition` возвращают bool — проверять. Результат — общий `DrawingAnnotationResult`.
|
||||
- `drawing_set_sheet_format` — `RequireLayoutSheet(n).Format` (`ISheetFormat`): `Format`=`ksDocumentFormatEnum` (`PaperFormat{A0..A5,User}`+`PaperFormats.Parse/ToKompas/FromKompas`), standard → set `VerticalOrientation=!landscape` (W/H auto-computed from enum+orientation, e.g. A3 landscape→420×297); **User → set `FormatWidth/Height` only, КОМПАС auto-derives `VerticalOrientation` from W/H and ignores the flag, never swaps W/H (spike)** → `sheet.Update()`. New drawing defaults A4 portrait. `ValidateFormatDimensions`: User needs W/H>0, standard forbids W/H (must be 0).
|
||||
- **Richer sketch primitives (API5 `ksDocument2D` wrappers)**: `ksArcBy3Points`; `ksArcByAngle` (centre/radius/start-end angles in degrees + counterClockwise flag); `ksEllipse` via `ksEllipseParam` (`KompasObject.GetParamStruct(ko_EllipseParam=22)`; **property names are `A`/`B` UPPERCASE in the interop**); `ksRegularPolygon` via `ksRegularPolygonParam` (`ko_RegularPolygonParam=92`; `describe = !inscribed`); NURBS spline via `ksNurbs(order=4)` + `ksNurbsPoint` loop + `ksEndObj`; `ksPoint`. Param structs released after use. `PartModeler` is partial: `.cs` (registry/helpers/`NewParam<T>`/reset), `.Sketch.cs` (2D primitives), `.Features.cs` (extrude/revolve/fillet/chamfer). Static `SketchGeometry` (`Core/Modeling`) holds enum maps + validators (`RequirePositive`, `RequireVertexCount`, `RequirePoints`, `RequireMin`). Point lists use `record SketchPoint(X, Y)` (JSON names `x`/`y`).
|
||||
|
||||
## КОМПАС-3D API architecture (the critical context)
|
||||
|
||||
КОМПАС-3D is automated via a **COM Automation API**, Windows-only. КОМПАС-3D **must be installed and running/launchable** on the same machine — the MCP server is a COM client, not a standalone CAD engine. There are **two coexisting API generations**:
|
||||
|
||||
| | Entry point | Namespace (C#) | Style | Use when |
|
||||
|---|---|---|---|---|
|
||||
| **API5** (legacy) | `KompasObject` | `Kompas6API5` | procedural, flat | older features, 2D primitives, bootstrapping |
|
||||
| **API7** (modern, preferred) | `IApplication` / `_Application` | `KompasAPI7` | OOP, interface-based | everything new — documents, 3D, parameters |
|
||||
|
||||
Constants live in separate libraries: `Kompas6Constants`, `Kompas6Constants3D` (C#), or `ksConstants.h` / `ksConstants3D.h` (C++).
|
||||
|
||||
### Connection pattern (verified from `Samples/CSharp.zip`)
|
||||
|
||||
1. Get the КОМПАС root object by COM ProgID. **Verified on this machine (v24 Home):**
|
||||
`KOMPAS.Application.7` and `KOMPAS.Application.5` are registered; `KOMPASLT.*` is **absent** —
|
||||
the Home edition uses the regular (non-LT) ProgIDs. Resolution order:
|
||||
- `KOMPAS.Application.7` — direct API7 `IApplication` (preferred)
|
||||
- `KOMPAS.Application.5` — API5 `KompasObject`, then `ksGetApplication7()`
|
||||
- `KOMPASLT.Application.5` — fallback for other installations
|
||||
- New instance: `Activator.CreateInstance(Type.GetTypeFromProgID(progId))`
|
||||
- Attach to a running instance: `Marshal.GetActiveObject(progId)`
|
||||
2. If you entered via API5, obtain the modern application: `IApplication appl = (IApplication)kompas.ksGetApplication7();` (via `KOMPAS.Application.7` you already hold `IApplication`).
|
||||
3. Set `appl.Visible = true` to show the КОМПАС window; drive documents via API7 interfaces (`IKompasDocument2D`, `IKompasDocument3D`, …).
|
||||
|
||||
> The SDK C# samples are mostly **КОМПАС plugin libraries** (DLLs loaded *into* КОМПАС as ActiveX, entry methods like `ExternalRunCommand`/`ExternalMenuItem`, registered via `[ComRegisterFunction]`). For an MCP server we want the **opposite direction**: a standalone *external automation client* that connects out-of-process via the ProgID pattern above. Read the samples for API usage, not for the plugin packaging/registration boilerplate.
|
||||
|
||||
## SDK layout (read-only reference, not part of this repo)
|
||||
|
||||
Under `C:\Program Files\ASCON\KOMPAS-3D v24 Home\SDK`:
|
||||
- `lib\*.tlb` — COM type libraries to reference / generate interop from: `kAPI5.tlb`, `kAPI7.tlb`, `ksConstants.tlb`, `ksConstants3D.tlb`, `kAPI2D5COM.tlb`, `kAPI3D5COM.tlb`.
|
||||
- `lib64\` — 64-bit import libs (`kAPI7.lib`, `kAPI5.lib`, …) and `.a` for C++/Builder.
|
||||
- `Include\` — C++/Pascal headers (`Ks_TLB.h`, `kAPI2D5COM.h`, `LDefin2D.pas`, …).
|
||||
- `KsAPI\` — the **KsAPI** (Qt/C++ cross-platform flavour): `Include\KsAPI.h`, `ksConstants*.h`, `Lib64\ksAPI.lib`, plus `Help\С чего начать.pdf` and a Linux/Debian 12 build guide.
|
||||
- `Samples\` — zipped examples per language: `CSharp.zip`, `C++.zip`, `Pascal.zip`, `Basic.zip`. Inside `CSharp.zip`, the `Step1`…`Step12` and `Step2_API7_2D` / `Step2_API7_3D` projects are the progressive tutorial; `Gayka`, `SlideWrk`, `EventsAuto` are larger feature demos.
|
||||
- `Help\KOMPAS_SDK_ru-RU.zip` and `KsAPI\Help\KsAPI_Help.zip` — offline copy of the docs.
|
||||
|
||||
To inspect a sample without unpacking the whole archive: `unzip -p "<path>\Samples\CSharp.zip" "Automation/Step2_API7_3D/Step2_API7_3D.cs"`. Note the `.cs` sample sources are **Windows-1251 encoded** (Cyrillic comments appear garbled in UTF-8 tools).
|
||||
|
||||
## Conventions / gotchas
|
||||
|
||||
- **Windows-only & 64-bit**: target x64 to match the installed КОМПАС; mixing bitness breaks COM activation. КОМПАС v24 Home is the installed edition — some full-edition API features may be unavailable.
|
||||
- **Encoding**: SDK source samples are CP1251. New project files should be UTF-8.
|
||||
- **COM lifetime**: release COM objects (`Marshal.ReleaseComObject`) / scope them; a leaked reference keeps the КОМПАС process alive.
|
||||
- **API choice**: prefer API7 for new tool implementations; drop to API5 only for things API7 doesn't expose.
|
||||
@@ -0,0 +1,115 @@
|
||||
using System.Runtime.Versioning;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace Kompas.Mcp.Core.Interop;
|
||||
|
||||
/// <summary>
|
||||
/// Поиск каталога установленного КОМПАС-3D по реестру COM.
|
||||
/// <para>
|
||||
/// Опорная точка — регистрация COM-сервера: <c>HKCR\<ProgID>\CLSID</c> →
|
||||
/// <c>HKCR\CLSID\{...}\LocalServer32</c>, значение которого содержит путь до исполняемого файла
|
||||
/// (проверено на v24 Home: <c>"C:\Program Files\ASCON\KOMPAS-3D v24 Home\Bin\kHome.Exe"</c>).
|
||||
/// Ветка <c>HKLM\SOFTWARE\ASCON\KOMPAS-3D</c> версионная и хранит настройки, а не путь установки,
|
||||
/// поэтому не используется: <c>LocalServer32</c> заодно гарантирует, что найденная установка —
|
||||
/// именно та, к которой сервер потом подключится по тому же ProgID.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class KompasInstallLocator
|
||||
{
|
||||
// Тот же порядок и тот же набор, что в KompasSession: .7 годится для поиска установки
|
||||
// (нужен только путь), хотя точкой входа при подключении служит API5.
|
||||
private static readonly string[] ProgIds =
|
||||
{
|
||||
"KOMPAS.Application.7",
|
||||
"KOMPAS.Application.5",
|
||||
"KOMPASLT.Application.5",
|
||||
};
|
||||
|
||||
/// <summary>Каталог установки КОМПАС или <c>null</c>, если COM-класс не зарегистрирован.</summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
public static string? TryFindInstallDirectory()
|
||||
{
|
||||
foreach (var progId in ProgIds)
|
||||
{
|
||||
var root = InstallRootFromServerCommand(TryReadLocalServer(progId));
|
||||
if (root is not null && Directory.Exists(root))
|
||||
return root;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
private static string? TryReadLocalServer(string progId)
|
||||
{
|
||||
using var progIdKey = Registry.ClassesRoot.OpenSubKey($@"{progId}\CLSID");
|
||||
if (progIdKey?.GetValue(null) is not string clsid || clsid.Length == 0)
|
||||
return null;
|
||||
|
||||
using var serverKey = Registry.ClassesRoot.OpenSubKey($@"CLSID\{clsid}\LocalServer32");
|
||||
return serverKey?.GetValue(null) as string;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Выделить путь до exe из значения <c>LocalServer32</c>: путь может быть в кавычках и
|
||||
/// сопровождаться аргументами командной строки (<c>...\kHome.Exe /automation</c>).
|
||||
/// </summary>
|
||||
public static string? ParseServerCommand(string? rawServerCommand)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rawServerCommand))
|
||||
return null;
|
||||
|
||||
var value = rawServerCommand.Trim();
|
||||
if (value[0] == '"')
|
||||
{
|
||||
var closing = value.IndexOf('"', 1);
|
||||
return closing > 1 ? value[1..closing] : null;
|
||||
}
|
||||
|
||||
// Без кавычек аргументы отделить надёжно нельзя (пробелы бывают и в пути) — режем
|
||||
// по расширению исполняемого файла, а не по первому пробелу.
|
||||
var extension = value.IndexOf(".exe", StringComparison.OrdinalIgnoreCase);
|
||||
return extension >= 0 ? value[..(extension + 4)] : value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Каталог установки по значению <c>LocalServer32</c>: <c><root>\Bin\kHome.Exe</c> →
|
||||
/// <c><root></c>. Если exe лежит не в <c>Bin</c>, возвращается его собственный каталог.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Разбор пути ручной, а не через <see cref="Path.GetDirectoryName(string)"/>: значение из
|
||||
/// реестра всегда Windows-пути, а unit-тесты гоняются в том числе на Linux-раннере, где
|
||||
/// <c>\</c> для <see cref="Path"/> — обычный символ.
|
||||
/// </remarks>
|
||||
public static string? InstallRootFromServerCommand(string? rawServerCommand)
|
||||
{
|
||||
var executable = ParseServerCommand(rawServerCommand);
|
||||
if (executable is null)
|
||||
return null;
|
||||
|
||||
var binDirectory = TrimLastSegment(executable);
|
||||
if (binDirectory is null)
|
||||
return null;
|
||||
|
||||
if (!string.Equals(LastSegment(binDirectory), "Bin", StringComparison.OrdinalIgnoreCase))
|
||||
return binDirectory;
|
||||
|
||||
return TrimLastSegment(binDirectory) ?? binDirectory;
|
||||
}
|
||||
|
||||
private static readonly char[] Separators = { '\\', '/' };
|
||||
|
||||
private static string? TrimLastSegment(string path)
|
||||
{
|
||||
var trimmed = path.TrimEnd(Separators);
|
||||
var separator = trimmed.LastIndexOfAny(Separators);
|
||||
return separator > 0 ? trimmed[..separator] : null;
|
||||
}
|
||||
|
||||
private static string LastSegment(string path)
|
||||
{
|
||||
var trimmed = path.TrimEnd(Separators);
|
||||
var separator = trimmed.LastIndexOfAny(Separators);
|
||||
return separator >= 0 ? trimmed[(separator + 1)..] : trimmed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace Kompas.Mcp.Core.Interop;
|
||||
|
||||
/// <summary>
|
||||
/// Интероп-сборки КОМПАС недоступны: сервер их не поставляет и берёт из установленного КОМПАС-3D.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Бросается лениво — из обработчика разрешения сборок, то есть в момент первого обращения к
|
||||
/// COM-типам (обычно это <c>kompas_connect</c>), а не при старте процесса. Сообщение пишется
|
||||
/// так, чтобы его можно было показать пользователю без доступа к исходникам.
|
||||
/// </remarks>
|
||||
public sealed class KompasInteropException : Exception
|
||||
{
|
||||
public KompasInteropException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
|
||||
namespace Kompas.Mcp.Core.Interop;
|
||||
|
||||
/// <summary>
|
||||
/// Подключает интероп-сборки КОМПАС из установленного экземпляра CAD, а не из поставки сервера.
|
||||
/// <para>
|
||||
/// Вызывается первой строкой <c>Program.Main</c>: обработчик
|
||||
/// <see cref="AssemblyLoadContext.Resolving"/> должен быть на месте до того, как JIT впервые
|
||||
/// коснётся COM-типов (это происходит лениво — обычно на <c>kompas_connect</c>). Сам поиск
|
||||
/// ленивый: регистрация обработчика ничего не ищет и не распаковывает.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class KompasInteropLoader
|
||||
{
|
||||
private static readonly object Gate = new();
|
||||
private static KompasInteropProvider? _provider;
|
||||
|
||||
/// <summary>Зарегистрировать резолвер. Повторные вызовы игнорируются.</summary>
|
||||
public static void Install(KompasInteropProvider? provider = null)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
if (_provider is not null)
|
||||
return;
|
||||
|
||||
_provider = provider ?? KompasInteropProvider.CreateDefault();
|
||||
AssemblyLoadContext.Default.Resolving += Resolve;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Каталог, из которого будут загружены сборки. Нужен для ранней диагностики при старте:
|
||||
/// иначе первый же вызов инструмента платил бы за распаковку архива SDK.
|
||||
/// </summary>
|
||||
/// <exception cref="KompasInteropException">Сборки не найдены.</exception>
|
||||
public static string ResolveDirectory()
|
||||
{
|
||||
var provider = _provider
|
||||
?? throw new InvalidOperationException($"Сначала вызовите {nameof(KompasInteropLoader)}.{nameof(Install)}().");
|
||||
|
||||
return provider.ResolveDirectory();
|
||||
}
|
||||
|
||||
private static Assembly? Resolve(AssemblyLoadContext context, AssemblyName name)
|
||||
{
|
||||
if (!KompasInteropProvider.IsKompasAssembly(name.Name))
|
||||
return null;
|
||||
|
||||
var path = Path.Combine(ResolveDirectory(), name.Name + ".dll");
|
||||
return File.Exists(path) ? context.LoadFromAssemblyPath(path) : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
using System.Globalization;
|
||||
using System.IO.Compression;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace Kompas.Mcp.Core.Interop;
|
||||
|
||||
/// <summary>
|
||||
/// Поиск каталога с интероп-сборками КОМПАС, которые сервер не поставляет вместе с собой.
|
||||
/// <para>
|
||||
/// Сборки (<c>KompasAPI7</c>, <c>Kompas6API5</c>, <c>Kompas6Constants</c>, <c>Kompas6Constants3D</c>)
|
||||
/// принадлежат АСКОН и распространяются в составе КОМПАС-3D, поэтому в релизный архив не входят —
|
||||
/// они берутся из установленного экземпляра. В установке отдельных DLL нет: они лежат внутри
|
||||
/// <c>SDK\Samples\CSharp.zip</c> в каталоге <c>Common/</c>, поэтому при первом обращении архив
|
||||
/// распаковывается в кеш <c>%LOCALAPPDATA%\kompas-mcp\interop\<ключ></c>.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class KompasInteropProvider
|
||||
{
|
||||
/// <summary>Переменная окружения с готовым каталогом сборок (обход поиска установки).</summary>
|
||||
public const string OverrideVariable = "KOMPAS_INTEROP_DIR";
|
||||
|
||||
private const string ZipEntryFolder = "Common/";
|
||||
|
||||
/// <summary>Имена сборок, которые сервер отдаёт из установки КОМПАС.</summary>
|
||||
/// <remarks>
|
||||
/// Компилируется код только против первых четырёх; остальные перечислены потому, что могут
|
||||
/// понадобиться как зависимости (<c>Kompas6API3D5COM</c> ссылается на <c>Kompas6API2D5COM</c>).
|
||||
/// </remarks>
|
||||
public static readonly IReadOnlyList<string> AssemblyNames = new[]
|
||||
{
|
||||
"KompasAPI7",
|
||||
"Kompas6API5",
|
||||
"Kompas6Constants",
|
||||
"Kompas6Constants3D",
|
||||
"Kompas6API2D5COM",
|
||||
"Kompas6API3D5COM",
|
||||
"KAPITypes",
|
||||
};
|
||||
|
||||
// Каталог считается пригодным, только если в нём есть все сборки, против которых собран Core.
|
||||
private static readonly string[] RequiredFiles =
|
||||
{
|
||||
"KompasAPI7.dll",
|
||||
"Kompas6API5.dll",
|
||||
"Kompas6Constants.dll",
|
||||
"Kompas6Constants3D.dll",
|
||||
};
|
||||
|
||||
private readonly string? _overrideDirectory;
|
||||
private readonly string _baseDirectory;
|
||||
private readonly string _cacheRoot;
|
||||
private readonly Func<string?> _installDirectory;
|
||||
private readonly object _gate = new();
|
||||
|
||||
private string? _resolved;
|
||||
|
||||
public KompasInteropProvider(
|
||||
string? overrideDirectory,
|
||||
string baseDirectory,
|
||||
string cacheRoot,
|
||||
Func<string?> installDirectory)
|
||||
{
|
||||
_overrideDirectory = overrideDirectory;
|
||||
_baseDirectory = baseDirectory;
|
||||
_cacheRoot = cacheRoot;
|
||||
_installDirectory = installDirectory ?? throw new ArgumentNullException(nameof(installDirectory));
|
||||
}
|
||||
|
||||
/// <summary>Провайдер с боевыми источниками: переменная окружения → каталог сервера → реестр.</summary>
|
||||
public static KompasInteropProvider CreateDefault() => new(
|
||||
Environment.GetEnvironmentVariable(OverrideVariable),
|
||||
AppContext.BaseDirectory,
|
||||
Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"kompas-mcp",
|
||||
"interop"),
|
||||
TryFindInstallDirectory);
|
||||
|
||||
/// <summary>Относится ли сборка с таким простым именем к интеропу КОМПАС.</summary>
|
||||
public static bool IsKompasAssembly(string? simpleName) =>
|
||||
simpleName is not null
|
||||
&& AssemblyNames.Any(name => string.Equals(name, simpleName, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
/// <summary>
|
||||
/// Каталог со сборками. Результат кешируется на время жизни процесса; при неудаче бросает
|
||||
/// <see cref="KompasInteropException"/> с текстом, пригодным для показа пользователю.
|
||||
/// </summary>
|
||||
public string ResolveDirectory()
|
||||
{
|
||||
lock (_gate)
|
||||
return _resolved ??= Locate();
|
||||
}
|
||||
|
||||
private string Locate()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(_overrideDirectory))
|
||||
{
|
||||
var directory = _overrideDirectory.Trim();
|
||||
if (HasRequiredFiles(directory))
|
||||
return directory;
|
||||
|
||||
throw new KompasInteropException(
|
||||
$"В каталоге из переменной {OverrideVariable} ({directory}) нет интероп-сборок КОМПАС " +
|
||||
$"({string.Join(", ", RequiredFiles)}). Уберите переменную, чтобы сервер взял сборки из " +
|
||||
"установленного КОМПАС-3D, либо укажите каталог, где эти файлы лежат.");
|
||||
}
|
||||
|
||||
// Сборки, положенные рядом с kompas-mcp.exe вручную, имеют приоритет над установкой:
|
||||
// это запасной путь для машин, где КОМПАС установлен без компонента SDK.
|
||||
if (HasRequiredFiles(_baseDirectory))
|
||||
return _baseDirectory;
|
||||
|
||||
var installDirectory = _installDirectory();
|
||||
if (string.IsNullOrWhiteSpace(installDirectory))
|
||||
throw new KompasInteropException(Unavailable(
|
||||
"КОМПАС-3D не найден: COM-класс KOMPAS.Application не зарегистрирован в системе."));
|
||||
|
||||
var archive = Path.Combine(installDirectory, "SDK", "Samples", "CSharp.zip");
|
||||
if (!File.Exists(archive))
|
||||
throw new KompasInteropException(Unavailable(
|
||||
$"в установке КОМПАС ({installDirectory}) нет компонента SDK — отсутствует файл {archive}."));
|
||||
|
||||
var cacheDirectory = Path.Combine(_cacheRoot, CacheKey(new FileInfo(archive)));
|
||||
if (HasRequiredFiles(cacheDirectory))
|
||||
return cacheDirectory;
|
||||
|
||||
Extract(archive, cacheDirectory);
|
||||
|
||||
if (!HasRequiredFiles(cacheDirectory))
|
||||
throw new KompasInteropException(Unavailable(
|
||||
$"в архиве {archive} нет ожидаемых сборок в каталоге {ZipEntryFolder}."));
|
||||
|
||||
return cacheDirectory;
|
||||
}
|
||||
|
||||
private static string Unavailable(string reason) =>
|
||||
"Не удалось найти интероп-сборки КОМПАС (" + string.Join(", ", RequiredFiles) + "). " +
|
||||
"Они не входят в поставку сервера и берутся из установленного КОМПАС-3D " +
|
||||
@"(SDK\Samples\CSharp.zip, каталог Common). Причина: " + reason + " " +
|
||||
"Установите КОМПАС-3D вместе с компонентом SDK или укажите каталог с этими сборками " +
|
||||
$"в переменной окружения {OverrideVariable}.";
|
||||
|
||||
/// <summary>
|
||||
/// Ключ кеша распаковки. Считается по самому архиву SDK, поэтому обновление КОМПАС (новый
|
||||
/// размер/время файла) даёт новый каталог кеша, а не подсовывает сборки от старой версии.
|
||||
/// </summary>
|
||||
public static string CacheKey(FileInfo archive)
|
||||
{
|
||||
var stamp = string.Join(
|
||||
'|',
|
||||
archive.FullName.ToLowerInvariant(),
|
||||
archive.Length.ToString(CultureInfo.InvariantCulture),
|
||||
archive.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(stamp)))[..16].ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static bool HasRequiredFiles(string? directory) =>
|
||||
!string.IsNullOrWhiteSpace(directory)
|
||||
&& Directory.Exists(directory)
|
||||
&& RequiredFiles.All(file => File.Exists(Path.Combine(directory, file)));
|
||||
|
||||
/// <summary>
|
||||
/// Распаковать <c>Common/*.dll</c> во временный каталог и атомарно переименовать его в целевой:
|
||||
/// два стартующих одновременно сервера не должны видеть наполовину распакованный кеш.
|
||||
/// </summary>
|
||||
private static void Extract(string archivePath, string targetDirectory)
|
||||
{
|
||||
var parent = Path.GetDirectoryName(targetDirectory)
|
||||
?? throw new KompasInteropException($"Некорректный путь кеша: {targetDirectory}");
|
||||
|
||||
Directory.CreateDirectory(parent);
|
||||
var staging = Path.Combine(parent, ".tmp-" + Guid.NewGuid().ToString("N")[..8]);
|
||||
Directory.CreateDirectory(staging);
|
||||
|
||||
try
|
||||
{
|
||||
using (var archive = ZipFile.OpenRead(archivePath))
|
||||
{
|
||||
foreach (var entry in archive.Entries)
|
||||
{
|
||||
if (!entry.FullName.StartsWith(ZipEntryFolder, StringComparison.OrdinalIgnoreCase)) continue;
|
||||
if (!entry.FullName.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)) continue;
|
||||
|
||||
entry.ExtractToFile(Path.Combine(staging, entry.Name), overwrite: true);
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Directory.Move(staging, targetDirectory);
|
||||
}
|
||||
catch (IOException) when (HasRequiredFiles(targetDirectory))
|
||||
{
|
||||
// Успел параллельный процесс — его результат ничем не хуже нашего.
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(staging)) Directory.Delete(staging, recursive: true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Мусор во временном каталоге не повод валить запуск сервера.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string? TryFindInstallDirectory()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
return KompasInstallLocator.TryFindInstallDirectory();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Недоступный/нестандартный реестр — трактуем как «установка не найдена».
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,23 +6,28 @@
|
||||
<AssemblyName>Kompas.Mcp.Core</AssemblyName>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Вендорские interop-сборки КОМПАС (libs/kompas-interop). -->
|
||||
<!--
|
||||
Interop-сборки КОМПАС нужны только для компиляции: Private=false исключает их из вывода
|
||||
build и publish, поэтому в релизный архив они не попадают (сборки принадлежат АСКОН и
|
||||
распространяются в составе КОМПАС-3D). В рантайме их подставляет KompasInteropLoader —
|
||||
из установленного КОМПАС (SDK\Samples\CSharp.zip → Common) или из KOMPAS_INTEROP_DIR.
|
||||
-->
|
||||
<ItemGroup>
|
||||
<Reference Include="KompasAPI7">
|
||||
<HintPath>$(KompasInteropDir)\KompasAPI7.dll</HintPath>
|
||||
<Private>true</Private>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="Kompas6API5">
|
||||
<HintPath>$(KompasInteropDir)\Kompas6API5.dll</HintPath>
|
||||
<Private>true</Private>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="Kompas6Constants">
|
||||
<HintPath>$(KompasInteropDir)\Kompas6Constants.dll</HintPath>
|
||||
<Private>true</Private>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="Kompas6Constants3D">
|
||||
<HintPath>$(KompasInteropDir)\Kompas6Constants3D.dll</HintPath>
|
||||
<Private>true</Private>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ using Kompas.Mcp.Core.Conversion;
|
||||
using Kompas.Mcp.Core.Documents;
|
||||
using Kompas.Mcp.Core.Drawings;
|
||||
using Kompas.Mcp.Core.Editing;
|
||||
using Kompas.Mcp.Core.Interop;
|
||||
using Kompas.Mcp.Core.Modeling;
|
||||
using Kompas.Mcp.Core.Query;
|
||||
using Kompas.Mcp.Core.Startup;
|
||||
@@ -14,6 +15,11 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
// Interop-сборки КОМПАС в поставку сервера не входят (принадлежат АСКОН) — они берутся из
|
||||
// установленного КОМПАС-3D. Резолвер вешается до любых обращений к COM-типам; сам поиск ленивый,
|
||||
// поэтому --version ниже работает и без установленного КОМПАС.
|
||||
KompasInteropLoader.Install();
|
||||
|
||||
// Печать версии — до поднятия хоста: это не MCP-сессия, stdout свободен.
|
||||
if (CliArguments.IsVersionRequest(args))
|
||||
{
|
||||
@@ -51,4 +57,19 @@ builder.Services
|
||||
.WithStdioServerTransport()
|
||||
.WithToolsFromAssembly();
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
var app = builder.Build();
|
||||
|
||||
// Ранняя диагностика: поиск установки и распаковка сборок здесь, а не внутри первого вызова
|
||||
// инструмента. Отсутствие сборок на старте не фатально — сообщение уйдёт в stderr, а падение
|
||||
// случится там, где его увидит агент: на первом обращении к КОМПАС.
|
||||
var interopLog = app.Services.GetRequiredService<ILoggerFactory>().CreateLogger("Kompas.Interop");
|
||||
try
|
||||
{
|
||||
interopLog.LogInformation("Interop-сборки КОМПАС: {Directory}", KompasInteropLoader.ResolveDirectory());
|
||||
}
|
||||
catch (KompasInteropException ex)
|
||||
{
|
||||
interopLog.LogWarning("{Message}", ex.Message);
|
||||
}
|
||||
|
||||
await app.RunAsync();
|
||||
|
||||
@@ -21,4 +21,13 @@
|
||||
<ProjectReference Include="..\..\src\Kompas.Mcp.Core\Kompas.Mcp.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!--
|
||||
Сервер интероп-сборки не поставляет и берёт их из установленного КОМПАС (см.
|
||||
Core/Interop/KompasInteropLoader.cs), но тестам они нужны рядом с тестовой сборкой:
|
||||
unit-тесты гоняются в том числе на Linux-раннере, где КОМПАС нет и подставить их неоткуда.
|
||||
-->
|
||||
<ItemGroup>
|
||||
<None Include="$(KompasInteropDir)\*.dll" CopyToOutputDirectory="PreserveNewest" Visible="false" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
using Kompas.Mcp.Core.Interop;
|
||||
|
||||
namespace Kompas.Mcp.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Разбор значения <c>LocalServer32</c>. Тесты кроссплатформенные намеренно: значения — всегда
|
||||
/// Windows-пути, но гоняются они и на Linux-раннере, поэтому разбор в локаторе не опирается
|
||||
/// на <c>Path</c>.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class KompasInstallLocatorTests
|
||||
{
|
||||
[Theory]
|
||||
// Реальное значение на v24 Home — путь в кавычках.
|
||||
[InlineData("\"C:\\Program Files\\ASCON\\KOMPAS-3D v24 Home\\Bin\\kHome.Exe\"",
|
||||
"C:\\Program Files\\ASCON\\KOMPAS-3D v24 Home\\Bin\\kHome.Exe")]
|
||||
[InlineData("C:\\ASCON\\Bin\\kompas.exe", "C:\\ASCON\\Bin\\kompas.exe")]
|
||||
// Без кавычек аргументы отрезаются по расширению, а не по первому пробелу.
|
||||
[InlineData("C:\\Program Files\\ASCON\\Bin\\kHome.Exe /automation",
|
||||
"C:\\Program Files\\ASCON\\Bin\\kHome.Exe")]
|
||||
public void ParseServerCommand_extracts_executable(string raw, string expected)
|
||||
=> Assert.Equal(expected, KompasInstallLocator.ParseServerCommand(raw));
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
public void ParseServerCommand_returns_null_for_empty(string? raw)
|
||||
=> Assert.Null(KompasInstallLocator.ParseServerCommand(raw));
|
||||
|
||||
[Fact]
|
||||
public void ParseServerCommand_returns_null_for_unclosed_quote()
|
||||
=> Assert.Null(KompasInstallLocator.ParseServerCommand("\"C:\\ASCON\\Bin\\kHome.Exe"));
|
||||
|
||||
[Fact]
|
||||
public void InstallRootFromServerCommand_strips_bin_subdirectory()
|
||||
{
|
||||
var root = KompasInstallLocator.InstallRootFromServerCommand(
|
||||
"\"C:\\Program Files\\ASCON\\KOMPAS-3D v24 Home\\Bin\\kHome.Exe\"");
|
||||
|
||||
Assert.Equal("C:\\Program Files\\ASCON\\KOMPAS-3D v24 Home", root);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InstallRootFromServerCommand_keeps_directory_when_not_bin()
|
||||
{
|
||||
var root = KompasInstallLocator.InstallRootFromServerCommand("\"C:\\ASCON\\KOMPAS\\kHome.Exe\"");
|
||||
|
||||
Assert.Equal("C:\\ASCON\\KOMPAS", root);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InstallRootFromServerCommand_returns_null_for_empty()
|
||||
=> Assert.Null(KompasInstallLocator.InstallRootFromServerCommand(null));
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
using System.IO.Compression;
|
||||
using Kompas.Mcp.Core.Interop;
|
||||
|
||||
namespace Kompas.Mcp.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Поиск интероп-сборок КОМПАС: приоритет источников, распаковка архива SDK, тексты ошибок.
|
||||
/// Настоящие DLL не нужны — провайдер проверяет только наличие файлов с нужными именами.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class KompasInteropProviderTests : IDisposable
|
||||
{
|
||||
private static readonly string[] RequiredFiles =
|
||||
{
|
||||
"KompasAPI7.dll",
|
||||
"Kompas6API5.dll",
|
||||
"Kompas6Constants.dll",
|
||||
"Kompas6Constants3D.dll",
|
||||
};
|
||||
|
||||
private readonly string _root = Path.Combine(Path.GetTempPath(), "kompas-interop-tests-" + Guid.NewGuid().ToString("N")[..8]);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try { if (Directory.Exists(_root)) Directory.Delete(_root, recursive: true); } catch { }
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("KompasAPI7", true)]
|
||||
[InlineData("kompas6api5", true)]
|
||||
[InlineData("Kompas6API3D5COM", true)]
|
||||
[InlineData("Kompas.Mcp.Core", false)]
|
||||
[InlineData(null, false)]
|
||||
public void IsKompasAssembly_matches_only_interop_names(string? name, bool expected)
|
||||
=> Assert.Equal(expected, KompasInteropProvider.IsKompasAssembly(name));
|
||||
|
||||
[Fact]
|
||||
public void Override_directory_wins_over_everything()
|
||||
{
|
||||
var overrideDir = CreateDirectoryWithAssemblies("override");
|
||||
var baseDir = CreateDirectoryWithAssemblies("base");
|
||||
|
||||
var provider = new KompasInteropProvider(overrideDir, baseDir, Cache, () => throw new Xunit.Sdk.XunitException(
|
||||
"поиск установки не должен вызываться при заданном KOMPAS_INTEROP_DIR"));
|
||||
|
||||
Assert.Equal(overrideDir, provider.ResolveDirectory());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Override_directory_without_assemblies_fails_loudly()
|
||||
{
|
||||
var empty = Path.Combine(_root, "empty");
|
||||
Directory.CreateDirectory(empty);
|
||||
|
||||
var provider = new KompasInteropProvider(empty, MissingDirectory, Cache, () => null);
|
||||
|
||||
var error = Assert.Throws<KompasInteropException>(() => provider.ResolveDirectory());
|
||||
Assert.Contains(KompasInteropProvider.OverrideVariable, error.Message, StringComparison.Ordinal);
|
||||
Assert.Contains(empty, error.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Base_directory_is_used_when_it_holds_assemblies()
|
||||
{
|
||||
var baseDir = CreateDirectoryWithAssemblies("base");
|
||||
|
||||
var provider = new KompasInteropProvider(null, baseDir, Cache, () => throw new Xunit.Sdk.XunitException(
|
||||
"поиск установки не должен вызываться, когда сборки лежат рядом с сервером"));
|
||||
|
||||
Assert.Equal(baseDir, provider.ResolveDirectory());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Missing_installation_reports_actionable_reason()
|
||||
{
|
||||
var provider = new KompasInteropProvider(null, MissingDirectory, Cache, () => null);
|
||||
|
||||
var error = Assert.Throws<KompasInteropException>(() => provider.ResolveDirectory());
|
||||
Assert.Contains("KOMPAS.Application", error.Message, StringComparison.Ordinal);
|
||||
Assert.Contains(KompasInteropProvider.OverrideVariable, error.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Installation_without_sdk_component_reports_missing_archive()
|
||||
{
|
||||
var install = Path.Combine(_root, "KOMPAS-3D v24 Home");
|
||||
Directory.CreateDirectory(install);
|
||||
|
||||
var provider = new KompasInteropProvider(null, MissingDirectory, Cache, () => install);
|
||||
|
||||
var error = Assert.Throws<KompasInteropException>(() => provider.ResolveDirectory());
|
||||
Assert.Contains("SDK", error.Message, StringComparison.Ordinal);
|
||||
Assert.Contains("CSharp.zip", error.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Archive_without_expected_entries_reports_empty_common_folder()
|
||||
{
|
||||
var install = CreateInstallation("KOMPAS-3D v24 Home", entries: new[] { "Automation/Interop.KGAXLib.dll" });
|
||||
|
||||
var provider = new KompasInteropProvider(null, MissingDirectory, Cache, () => install);
|
||||
|
||||
var error = Assert.Throws<KompasInteropException>(() => provider.ResolveDirectory());
|
||||
Assert.Contains("Common/", error.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Assemblies_are_extracted_from_sdk_archive_into_cache()
|
||||
{
|
||||
var install = CreateInstallation("KOMPAS-3D v24 Home", entries: CommonEntries("readme.txt"));
|
||||
|
||||
var directory = new KompasInteropProvider(null, MissingDirectory, Cache, () => install).ResolveDirectory();
|
||||
|
||||
Assert.StartsWith(Cache, directory, StringComparison.Ordinal);
|
||||
Assert.All(RequiredFiles, file => Assert.True(File.Exists(Path.Combine(directory, file)), file));
|
||||
// В кеш попадают только DLL из Common — не весь архив.
|
||||
Assert.False(File.Exists(Path.Combine(directory, "readme.txt")));
|
||||
Assert.False(File.Exists(Path.Combine(directory, "Interop.KGAXLib.dll")));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extracted_cache_is_reused_by_the_next_process()
|
||||
{
|
||||
var install = CreateInstallation("KOMPAS-3D v24 Home", entries: CommonEntries());
|
||||
|
||||
var first = new KompasInteropProvider(null, MissingDirectory, Cache, () => install).ResolveDirectory();
|
||||
var marker = Path.Combine(first, "marker.txt");
|
||||
File.WriteAllText(marker, "распаковано первым запуском");
|
||||
|
||||
var second = new KompasInteropProvider(null, MissingDirectory, Cache, () => install).ResolveDirectory();
|
||||
|
||||
Assert.Equal(first, second);
|
||||
Assert.True(File.Exists(marker), "кеш должен переиспользоваться, а не распаковываться заново");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cache_key_follows_the_sdk_archive()
|
||||
{
|
||||
var archive = Path.Combine(_root, "CSharp.zip");
|
||||
Directory.CreateDirectory(_root);
|
||||
File.WriteAllText(archive, "v24");
|
||||
var before = KompasInteropProvider.CacheKey(new FileInfo(archive));
|
||||
|
||||
// Обновление КОМПАС меняет архив — ключ обязан смениться, иначе подхватится старый кеш.
|
||||
File.WriteAllText(archive, "v25 — другой размер");
|
||||
File.SetLastWriteTimeUtc(archive, DateTime.UnixEpoch.AddDays(1));
|
||||
var after = KompasInteropProvider.CacheKey(new FileInfo(archive));
|
||||
|
||||
Assert.NotEqual(before, after);
|
||||
Assert.Equal(16, before.Length);
|
||||
}
|
||||
|
||||
private string Cache => Path.Combine(_root, "cache");
|
||||
|
||||
private string MissingDirectory => Path.Combine(_root, "no-such-directory");
|
||||
|
||||
private static string[] CommonEntries(params string[] extra)
|
||||
=> RequiredFiles.Select(file => "Common/" + file)
|
||||
.Concat(extra.Select(name => "Common/" + name))
|
||||
.Append("Automation/Interop.KGAXLib.dll")
|
||||
.ToArray();
|
||||
|
||||
private string CreateDirectoryWithAssemblies(string name)
|
||||
{
|
||||
var directory = Path.Combine(_root, name);
|
||||
Directory.CreateDirectory(directory);
|
||||
foreach (var file in RequiredFiles)
|
||||
File.WriteAllText(Path.Combine(directory, file), "заглушка");
|
||||
|
||||
return directory;
|
||||
}
|
||||
|
||||
private string CreateInstallation(string name, string[] entries)
|
||||
{
|
||||
var install = Path.Combine(_root, name);
|
||||
var samples = Path.Combine(install, "SDK", "Samples");
|
||||
Directory.CreateDirectory(samples);
|
||||
|
||||
using var archive = ZipFile.Open(Path.Combine(samples, "CSharp.zip"), ZipArchiveMode.Create);
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
using var writer = new StreamWriter(archive.CreateEntry(entry).Open());
|
||||
writer.Write("заглушка");
|
||||
}
|
||||
|
||||
return install;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user