feat(host): MCP-сервер на stdio + 18 инструментов

- Program.cs: Host + DI (KompasDispatcher/Session/DocumentService/PartModeler/
  SnapshotService), MCP на stdio, логи в stderr
- Tools: System (connect/status/set_visible), Documents (create/open/save/
  save_as/close/active), Sketch (create/add_line/circle/rectangle/close),
  Features (extrude_boss/extrude_cut/rebuild), Vision (model_snapshot -> ImageContentBlock)
- KompasSession: SetVisibleAsync + кэш LastInfo
- smoke: tools/list по stdio возвращает все 18 инструментов

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 23:57:56 +03:00
parent af6adc390e
commit 68855c218e
7 changed files with 265 additions and 4 deletions
+9 -2
View File
@@ -43,6 +43,13 @@ public sealed class KompasSession : IDisposable
public bool IsConnected => _app is not null;
/// <summary>Сведения о последнем успешном подключении (null, пока не подключались).</summary>
public KompasConnectionInfo? LastInfo { get; private set; }
/// <summary>Показать/скрыть окно КОМПАС.</summary>
public Task SetVisibleAsync(bool visible, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => Application.Visible = visible, ct);
/// <summary>Корневой объект API5. Доступен только на STA-потоке после подключения.</summary>
public KompasObject Kompas =>
_kompas ?? throw new InvalidOperationException("Нет подключения к КОМПАС. Сначала вызовите ConnectAsync.");
@@ -58,7 +65,7 @@ public sealed class KompasSession : IDisposable
private KompasConnectionInfo ConnectCore(bool visible)
{
if (_app is not null)
return BuildInfo(LastProgId);
return LastInfo ??= BuildInfo(LastProgId);
// 1) attach к уже запущенному экземпляру
foreach (var id in ProgIds)
@@ -107,7 +114,7 @@ public sealed class KompasSession : IDisposable
_app.Visible = visible;
return BuildInfo(LastProgId);
return LastInfo = BuildInfo(LastProgId);
}
private string LastProgId { get; set; } = string.Empty;
+29 -2
View File
@@ -1,2 +1,29 @@
// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");
using Kompas.Mcp.Core;
using Kompas.Mcp.Core.Documents;
using Kompas.Mcp.Core.Modeling;
using Kompas.Mcp.Core.Threading;
using Kompas.Mcp.Core.Vision;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
var builder = Host.CreateApplicationBuilder(args);
// ВАЖНО: stdout занят протоколом MCP (stdio). Все логи — только в stderr.
builder.Logging.AddConsole(o => o.LogToStandardErrorThreshold = LogLevel.Trace);
builder.Logging.SetMinimumLevel(LogLevel.Information);
// COM-слой: один STA-поток, одно подключение, общие сервисы (синглтоны).
builder.Services.AddSingleton<KompasDispatcher>();
builder.Services.AddSingleton<KompasSession>();
builder.Services.AddSingleton<DocumentService>();
builder.Services.AddSingleton<PartModeler>();
builder.Services.AddSingleton<SnapshotService>();
// MCP-сервер поверх stdio; инструменты находятся по атрибутам в этой сборке.
builder.Services
.AddMcpServer()
.WithStdioServerTransport()
.WithToolsFromAssembly();
await builder.Build().RunAsync();
@@ -0,0 +1,69 @@
using System.ComponentModel;
using Kompas.Mcp.Core;
using Kompas.Mcp.Core.Documents;
using ModelContextProtocol.Server;
namespace Kompas.Mcp.Host.Tools;
/// <summary>Инструменты управления документами.</summary>
[McpServerToolType]
public sealed class DocumentTools(KompasSession session, DocumentService docs)
{
[McpServerTool(Name = "document_create")]
[Description("Создать новый документ и сделать его активным. type: part | assembly | drawing | fragment | specification | text.")]
public async Task<string> Create([Description("Тип документа")] string type)
{
await session.ConnectAsync();
var info = await docs.CreateAsync(DocumentTypes.Parse(type));
return $"Создан документ типа {info.Type}.";
}
[McpServerTool(Name = "document_open")]
[Description("Открыть документ КОМПАС с диска по полному пути.")]
public async Task<string> Open(
[Description("Полный путь к файлу")] string path,
[Description("Открыть только для чтения")] bool readOnly = false)
{
await session.ConnectAsync();
var info = await docs.OpenAsync(path, readOnly);
return $"Открыт документ: {info.Name} ({info.Type}).";
}
[McpServerTool(Name = "document_save_as")]
[Description("Сохранить активный документ по указанному пути. Безопасно для новых документов. Расширения: .m3d (деталь), .a3d (сборка), .cdw (чертёж), .frw (фрагмент).")]
public async Task<string> SaveAs([Description("Полный путь файла с расширением")] string path)
{
await session.ConnectAsync();
await docs.SaveAsAsync(path);
return $"Сохранено: {path}";
}
[McpServerTool(Name = "document_save")]
[Description("Сохранить активный документ (для уже сохранённого ранее).")]
public async Task<string> Save()
{
await session.ConnectAsync();
await docs.SaveAsync();
return "Документ сохранён.";
}
[McpServerTool(Name = "document_close")]
[Description("Закрыть активный документ. save — сохранять ли изменения при закрытии.")]
public async Task<string> Close([Description("Сохранять изменения")] bool save = false)
{
await session.ConnectAsync();
var ok = await docs.CloseAsync(save);
return ok ? "Документ закрыт." : "Не удалось закрыть документ.";
}
[McpServerTool(Name = "document_active")]
[Description("Сведения об активном документе.")]
public async Task<string> Active()
{
await session.ConnectAsync();
var i = await docs.GetActiveAsync();
if (i is null) return "Нет активного документа.";
var where = string.IsNullOrEmpty(i.PathName) ? "не сохранён" : i.PathName;
return $"{i.Type}: {i.Name} ({where}).";
}
}
+42
View File
@@ -0,0 +1,42 @@
using System.ComponentModel;
using Kompas.Mcp.Core;
using Kompas.Mcp.Core.Modeling;
using ModelContextProtocol.Server;
namespace Kompas.Mcp.Host.Tools;
/// <summary>Инструменты формообразующих операций.</summary>
[McpServerToolType]
public sealed class FeatureTools(KompasSession session, PartModeler modeler)
{
[McpServerTool(Name = "extrude_boss")]
[Description("Выдавить эскиз, добавив материал (на глубину depth, мм). Возвращает id операции.")]
public async Task<string> ExtrudeBoss(int sketchId, double depth, [Description("Прямое направление")] bool forward = true)
{
await session.ConnectAsync();
var id = await modeler.ExtrudeAsync(sketchId, depth, cut: false, forward: forward);
return $"Выдавливание (boss) создано, id={id}.";
}
[McpServerTool(Name = "extrude_cut")]
[Description("Вырезать выдавливанием по эскизу. throughAll=true — насквозь (для отверстий, depth игнорируется); иначе на глубину depth.")]
public async Task<string> ExtrudeCut(
int sketchId,
[Description("Глубина, мм (если не throughAll)")] double depth = 0,
[Description("Вырезать насквозь")] bool throughAll = true,
[Description("Прямое направление")] bool forward = true)
{
await session.ConnectAsync();
var id = await modeler.ExtrudeAsync(sketchId, depth <= 0 ? 1 : depth, cut: true, forward: forward, throughAll: throughAll);
return $"Вырез (cut) создан, id={id}.";
}
[McpServerTool(Name = "rebuild")]
[Description("Перестроить активный документ.")]
public async Task<string> Rebuild()
{
await session.ConnectAsync();
await modeler.RebuildAsync();
return "Документ перестроен.";
}
}
+56
View File
@@ -0,0 +1,56 @@
using System.ComponentModel;
using Kompas.Mcp.Core;
using Kompas.Mcp.Core.Modeling;
using ModelContextProtocol.Server;
namespace Kompas.Mcp.Host.Tools;
/// <summary>Инструменты построения эскизов в активной детали.</summary>
[McpServerToolType]
public sealed class SketchTools(KompasSession session, PartModeler modeler)
{
[McpServerTool(Name = "sketch_create")]
[Description("Создать эскиз на базовой плоскости (XOY, XOZ или YOZ) активной детали и открыть его на редактирование. Возвращает id эскиза для последующих sketch_add_* и операций.")]
public async Task<string> Create([Description("Плоскость: XOY, XOZ или YOZ")] string plane)
{
await session.ConnectAsync();
var id = await modeler.OpenSketchAsync(BasePlanes.Parse(plane));
return $"Эскиз создан, id={id}. Добавьте геометрию (sketch_add_*) и завершите sketch_close.";
}
[McpServerTool(Name = "sketch_add_circle")]
[Description("Добавить окружность в открытый эскиз. Координаты — в плоскости эскиза, мм.")]
public async Task<string> AddCircle(int sketchId, double centerX, double centerY, double radius)
{
await session.ConnectAsync();
await modeler.AddCircleAsync(sketchId, centerX, centerY, radius);
return "Окружность добавлена.";
}
[McpServerTool(Name = "sketch_add_line")]
[Description("Добавить отрезок в открытый эскиз (две точки, мм).")]
public async Task<string> AddLine(int sketchId, double x1, double y1, double x2, double y2)
{
await session.ConnectAsync();
await modeler.AddLineAsync(sketchId, x1, y1, x2, y2);
return "Отрезок добавлен.";
}
[McpServerTool(Name = "sketch_add_rectangle")]
[Description("Добавить прямоугольник по двум противоположным углам в открытый эскиз (мм).")]
public async Task<string> AddRectangle(int sketchId, double x1, double y1, double x2, double y2)
{
await session.ConnectAsync();
await modeler.AddRectangleAsync(sketchId, x1, y1, x2, y2);
return "Прямоугольник добавлен.";
}
[McpServerTool(Name = "sketch_close")]
[Description("Завершить редактирование эскиза (перед операцией выдавливания).")]
public async Task<string> Close(int sketchId)
{
await session.ConnectAsync();
await modeler.CloseSketchAsync(sketchId);
return "Эскиз закрыт.";
}
}
+34
View File
@@ -0,0 +1,34 @@
using System.ComponentModel;
using Kompas.Mcp.Core;
using ModelContextProtocol.Server;
namespace Kompas.Mcp.Host.Tools;
/// <summary>Системные инструменты: подключение, статус, видимость окна.</summary>
[McpServerToolType]
public sealed class SystemTools(KompasSession session)
{
[McpServerTool(Name = "kompas_connect")]
[Description("Подключиться к КОМПАС-3D: присоединиться к запущенному экземпляру или запустить новый, показать окно. Возвращает версию и редакцию.")]
public async Task<string> Connect()
{
var info = await session.ConnectAsync();
return $"Подключено к КОМПАС {info.Version} (build {info.BuildNumber}, {info.Edition}), ProgID {info.ProgId}.";
}
[McpServerTool(Name = "kompas_status")]
[Description("Текущее состояние подключения к КОМПАС.")]
public string Status()
=> session.IsConnected && session.LastInfo is { } i
? $"Подключено: КОМПАС {i.Version} ({i.Edition})."
: "Не подключено. Вызовите kompas_connect.";
[McpServerTool(Name = "kompas_set_visible")]
[Description("Показать или скрыть главное окно КОМПАС.")]
public async Task<string> SetVisible([Description("true — показать, false — скрыть")] bool visible)
{
await session.ConnectAsync();
await session.SetVisibleAsync(visible);
return visible ? "Окно КОМПАС показано." : "Окно КОМПАС скрыто.";
}
}
+26
View File
@@ -0,0 +1,26 @@
using System.ComponentModel;
using Kompas.Mcp.Core;
using Kompas.Mcp.Core.Vision;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
namespace Kompas.Mcp.Host.Tools;
/// <summary>«Зрение агента»: снимок 3D-модели.</summary>
[McpServerToolType]
public sealed class VisionTools(KompasSession session, SnapshotService snapshot)
{
[McpServerTool(Name = "model_snapshot")]
[Description("Снять текущий вид активной 3D-модели в PNG и вернуть как изображение — чтобы увидеть результат построения и решить следующий шаг.")]
public async Task<ImageContentBlock> Snapshot(
[Description("Разрешение в DPI; 0 — экранное")] int resolution = 0)
{
await session.ConnectAsync();
var snap = await snapshot.CaptureActiveModelAsync(resolution: resolution);
return new ImageContentBlock
{
Data = snap.Bytes,
MimeType = snap.MimeType,
};
}
}