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:
@@ -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;
|
||||
|
||||
@@ -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}).";
|
||||
}
|
||||
}
|
||||
@@ -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 "Документ перестроен.";
|
||||
}
|
||||
}
|
||||
@@ -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 "Эскиз закрыт.";
|
||||
}
|
||||
}
|
||||
@@ -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 ? "Окно КОМПАС показано." : "Окно КОМПАС скрыто.";
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user