内容
# svMCP — Tekla Structures MCP 服务器
MCP(模型上下文协议)服务器,用于通过 Claude Desktop 等 MCP 客户端与 **Tekla Structures 2021 和 2025** 协同工作。
## 要求
- Windows 10/11
- .NET 8 SDK
- .NET Framework 4.8(包含在 Windows 10+ 中)
- 已安装并运行的 Tekla Structures **2021** 或 **2025**
## 安装和配置
### 1. 编译
```bash
dotnet build src/TeklaMcpServer/TeklaMcpServer.csproj -c Release
```
自动编译 `TeklaMcpServer`(net8.0)和 `TeklaBridge`(net48),并将 TeklaBridge.exe 复制到 `bin/Release/net8.0-windows/bridge/`。
### 2. 配置 Claude Desktop
`%APPDATA%\Claude\claude_desktop_config.json`:
```json
{
"mcpServers": {
"tekla": {
"command": "D:\\repos\\svMCP\\src\\TeklaMcpServer\\bin\\Release\\net8.0-windows\\TeklaMcpServer.exe"
}
}
}
```
### 3. 部署后更改
```bash
# 1. 关闭 Claude Desktop
# 2. 编译
dotnet build src/TeklaMcpServer/TeklaMcpServer.csproj -c Release
# 3. 打开 Claude Desktop
```
> **重要:** 在 Claude Desktop 打开时,TeklaMcpServer.exe 被阻止。
> 如果只有 TeklaBridge 被修改,可以在不关闭 Claude Desktop 的情况下重新编译它,
> 结果将自动复制到 `bridge/` 目录下。
### 适用于 Tekla Structures 2025 的附加信息
TeklaBridge 必须从 Tekla 的扩展目录启动,而不是从标准 `bridge/` 目录。
这是因为 Tekla 生成的 `exe.config` 包含 `<codeBase>` 记录,
这些记录指向具有正确版本的已安装 DLL(FileVersion `2025.0.52577.0`,而不是 NuGet `2025.0.0.0`)。
扩展目录:`C:\TeklaStructures\2025.0\Environments\common\extensions\svMCP\`
**部署 TeklaBridge 以适应 TS2025:**
```bash
# 编译 TeklaBridge(无需关闭 Claude Desktop)
dotnet build src/TeklaBridge/TeklaBridge.csproj -c Release
# 将 TeklaBridge.exe 和 TeklaMcpServer.Api.dll 复制到扩展目录
$src = "src\TeklaBridge\bin\Release\net48"
$dst = "C:\TeklaStructures\2025.0\Environments\common\extensions\svMCP"
New-Item -ItemType Directory -Force $dst
Copy-Item "$src\TeklaBridge.exe" $dst
Copy-Item "$src\TeklaMcpServer.Api.dll" $dst
# 还要复制其他依赖项(System.Text.Json、Newtonsoft.Json 等)
```
**exe.config:** 需要手动创建一次并存储在扩展目录中。
包含所有 Tekla DLL 的 `<bindingRedirect>` 和 `<codeBase>` 记录,
这些记录指向 `C:\TeklaStructures\2025.0\bin\`。
生成器:`C:\temp\GenConfig2.ps1`。
TeklaMcpServer 自动检测 TS2025 的存在并从扩展目录启动桥梁,
如果文件存在(`ResolveBridgePath()` 在 `ModelTools.Shared.cs` 中)。
## 架构
```
Claude Desktop
│ stdio (JSON-RPC / MCP)
▼
TeklaMcpServer.exe (net8.0-windows)
│ Process.Start → stdout pipe
▼
TeklaBridge.exe (net48)
│ .NET Remoting IPC (TS2021) / Trimble.Remoting MMF (TS2025)
▼
Tekla Structures 2021 / 2025
```
### 为什么有两个进程?
Tekla Structures Open API 需要 **net48**。
MCP SDK 需要 **net8+**。无法在单个进程中合并它们 - 不同的运行时,不同的 CLR。
| 版本 | IPC 机制 | 启动特征 |
|---|---|---|
| TS2021 | .NET Remoting,命名管道 | 反射修复通道名称(后缀 `-Console:`) |
| TS2025 | Trimble.Remoting,内存映射文件 | 从扩展目录启动并使用 `exe.config` + `<codeBase>` |
TeklaBridge 是一个薄的 net48 包装器:接受命令作为第一个参数,执行 Tekla API,并将 JSON 返回到 stdout。
## 项目结构
```
src/
├── TeklaMcpServer.Api/ # 所有 Tekla API 代码(net48)- 接口,DTO,实现
│ ├── Selection/ # IModelSelectionApi,ModelObjectInfo,TeklaModelSelectionApi
│ │ # ISelectionCacheManager,SelectionCacheManager
│ │ # SelectionResult,ToolInputSelectionHandler
│ ├── Drawing/ # 通用绘图层:DrawingContext,DrawingViewContext,
│ │ # MarksViewContext,API/DTO/构建器用于视图/标记/尺寸
│ │ # DrawingViewInfo,DrawingViewsResult,DrawingMarkInfo,...
│ ├── Algorithms/
│ │ ├── Packing/ # MaxRectsBinPacker
│ │ └── Marks/ # MarkLayoutEngine,候选人生成,评分,重叠解析器
│ └── Filtering/
│ ├── Common/ # FilterExpressionParser,FilterTokenizer,FilterAstBuilder,FilterHelper…
│ ├── Drawing/ # DrawingObjectsFilterHelper
│ └── Model/ # IModelFilteringApi,DTOs,TeklaModelFilteringApi
├── TeklaMcpServer/ # MCP 服务器(net8.0-windows)
│ ├── Program.cs # 入口点,MCP 主机配置
│ └── Tools/ # 薄 MCP 包装器
│ ├── Shared/ # RunBridge(),公共代码
│ ├── Connection/ # check_connection
│ ├── Model/ # 模型工具
│ └── Drawing/ # 绘图工具
├── TeklaBridge/ # 桥梁进程(net48)- 仅调度命令
│ ├── Program.cs # 入口点 + IPC 修复 + 控制台捕获
│ └── Commands/
│ ├── ModelCommandHandler.cs
│ └── DrawingCommandHandler.cs
└── svMCP/ # 存根(未使用)
```
### 责任分工
| 层 | 项目 | 角色 |
|---|---|---|
| MCP 工具 | `TeklaMcpServer/Tools/` | **仅薄包装器** - 调用 `RunBridge()`,解析 JSON,返回字符串 |
| 桥梁调度器 | `TeklaBridge/Commands/` | 将命令调度到 `TeklaMcpServer.Api` 类,序列化结果 |
| 所有 Tekla 代码 | `TeklaMcpServer.Api/` | 接口,DTO 和所有 Tekla API 实现 |
`TeklaMcpServer.Api` 是一个单独的 net48 项目(不与 net8 服务器共享),其价值在于桥接进程中合同的明确边界:可审查性,通过模拟进行测试,并支持多个 Tekla 版本。
## 可用工具
### 连接
| 工具 | 描述 |
|---|---|
| `check_connection` | 检查与 Tekla Structures 的连接;返回模型名称和路径 |
### 模型
| 工具 | 描述 |
|---|---|
| `get_selected_elements_properties` | 所选元素的属性:Part、BoltGroup、Weld、RebarGroup - 所有类型 |
| `get_selected_elements_total_weight` | 所选元素的总重量(千克) |
| `select_elements_by_class` | 按 Tekla 类号选择元素 |
| `filter_model_objects_by_type` | 按类型查找和选择模型对象(beam、plate、bolt、assembly...) |
### 绘图
> 需要在 Tekla Structures 中打开 Drawing Editor
| 工具 | 描述 |
|---|---|
| `list_drawings` | 模型的绘图列表 |
| `find_drawings` | 按名称和/或标记搜索(包含,不区分大小写) |
| `find_drawings_by_properties` | 按多个属性搜索(JSON 过滤器) |
| `open_drawing` | 按 GUID 打开(激活)绘图 |
| `close_drawing` | 关闭活动绘图 |
| `export_drawings_to_pdf` | 将绘图导出为 PDF,按 GUID |
| `create_general_arrangement_drawing` | 旧解决方法:通过宏从保存的模型视图创建 GA 绘图 |
| `create_single_part_drawing` | 通过 Tekla Open API 创建 Single Part 绘图 |
| `create_assembly_drawing` | 通过 Tekla Open API 创建 Assembly 绘图 |
| `get_drawing_context` | 活动绘图和所选对象 |
| `get_drawing_layout_context` | Coarse `DrawingContext`:绘图/图纸/视图/预留布局 |
| `get_drawing_view_context` | 详细 `DrawingViewContext` 用于单个视图 |
| `get_sheet_objects_debug` | Dev/debug:所有图纸对象、边界框和候选预留区域 |
| `select_drawing_objects` | 按模型对象 ID 选择图纸对象 |
| `filter_drawing_objects` | 按类型过滤图纸对象(Mark、Part、DimensionBase...) |
| `set_mark_content` | 更改标记内容和字体 |
| `get_drawing_views` | 活动绘图的所有视图:位置、比例、大小、图纸大小 |
| `get_drawing_reserved_areas` | 工作表的预留区域:表格、边距和最终阻塞区域 |
| `get_drawing_section_sides` | 分段视图的边相对于基本视图 |
| `move_view` | 在图纸上移动视图(绝对或偏移) |
| `set_view_scale` | 更改一个或多个视图的比例 |
| `fit_views_to_sheet` | 作者设置视图的主要路径:标准比例、正交布局,无重叠和投影对齐 |
| `get_drawing_marks` | 读取标记:位置、边界框/OBB、`resolvedGeometry`、重叠、引导线、PropertyElement 内容;按视图过滤 |
| `create_part_marks` | 创建具有指定内容和样式的零件标记 |
| `delete_all_marks` | 删除活动绘图上的所有标记 |
| `get_drawing_parts` | 图纸的所有模型对象:PART_POS、ASSEMBLY_POS、PROFILE、MATERIAL、NAME |
| `get_drawing_dimensions` | 活动绘图的所有 `StraightDimensionSet`:id、`dimensionType`、distance、`viewId/viewType`、orientation、`direction`、`topDirection`、`referenceLine`、边界框集/段、`dimensionLine`、`leadLineMain/Second`、`textBounds` |
| `get_dimension_contexts` | 尺寸的内部/上下文读取路径 |
| `arrange_dimensions` | 通过 `Distance` 确定平行尺寸堆栈的间距 |
| `combine_dimensions` | 兼容尺寸集的受控组合路径 |
| `move_dimension` | 将尺寸线移动到 delta(更改 `StraightDimensionSet.Distance`) |
| `create_dimension` | 按点集创建 `StraightDimensionSet` |
| `delete_dimension` | 按 ID 删除 `StraightDimensionSet` |
| `place_control_diagonals` | 根据可见零件的实际实体几何创建控制对角线尺寸;按 `MATERIAL_TYPE` 过滤(默认:钢/混凝土/木材,无绝缘); 方向始终从下到上 |
| `get_part_geometry_in_view` | 在视图的本地坐标系中获取零件几何(边界框、起点/终点、轴) |
| `get_all_parts_geometry_in_view` | 批量获取视图中所有零件的几何形状 |
| `get_grid_axes` | 在指定的绘图视图中获取网格轴 |
| `resolve_mark_overlaps` | 自动解决视图内文本块的冲突 - 最小的本地偏移 |
| `arrange_marks` | 视图内每个视图的完整自动标记布局 |
| `arrange_marks_no_collisions` | 组合遍历:`arrange_marks` + 重复 `resolve_mark_overlaps` 直到稳定 |
| `draw_debug_overlay` | Dev/debug:在绘图中绘制临时叠加层(线/多边形/文本/十字) |
| `clear_debug_overlay` | Dev/debug:清除叠加层或按组 |
布局标记当前设置为:
- 处理单元 - 一个 `View` 而不是整个绘图纸
- 实际标记层现在作为 `MarksViewContext` + `MarksViewContextBuilder` 格式化
- `get_drawing_marks` 已经构建在上下文层之上,但保留旧的 DTO `DrawingMarkInfo`
- 所有布局引擎计算都在视图的本地坐标系中进行
- Tekla 层仅收集中性 `MarkLayoutItem` 并将偏移量转换为视图和图纸坐标之间的偏移量
- `resolve_mark_overlaps` 仅使用本地 `MarkOverlapResolver` 进行最小偏移
- `arrange_marks` 使用 `MarkLayoutEngine`:候选人生成、评分、贪婪放置和本地重叠解析器
- 对于引导线标记,锚点取自 `LeaderLinePlacing.StartPoint`(视图坐标系);`StartPoint` 不更改,仅更改 `InsertionPoint`
- 标记几何中心化在 `MarkGeometryHelper` 中,取决于 `PlacingType`:
- `LeaderLinePlacing` - 标记的 object-aligned 几何形状
- `BaseLinePlacing` / `AlongLinePlacing` - 相关零件在当前视图的方向
- fallback 路径 - 原始 Tekla 几何形状,但不被视为标记冲突/布局推理的规范
绘图层上下文当前设置为:
- `DrawingContext` - 粗糙的图纸级上下文,用于布局视图、预留区域和 `before/after` 案例
- `DrawingViewContext` - 详细的每个视图的几何上下文
- `MarksViewContext` - 视图的实际标记上下文
布局视图当前设置为:
- 旧的 `PlaceViews()` 已从 MCP/bridge 层中删除,不再是支持的工具
- 视图的主要设置路径 - `fit_views_to_sheet`
- 表格级推理现在依赖于 `DrawingContext`(`get_drawing_layout_context`)
- 投影对齐后处理在 `DrawingProjectionAlignmentService` 内执行
- 表格预留区域:`DrawingReservedAreaReader.ReadLayoutTableGeometries()` 使用 `TableLayout.GetCurrentTables()` → `PresentationConnection.GetObjectPresentation()` → 画布标记段 `Segment.Primitives[0/2]` → 精确的边界框。表格与 `OverlapVithViews=true` 的表格被跳过。
- 边界框:`TableLayout.GetMarginsAndSpaces()` 返回实际边距(通常 5-10mm),用作 `sheetMargin` 在响应中
- 边界框:`Segment.Primitives[0/2]` 是边界框的规范源;允许累积图元仅作为后备
尺寸当前设置为:
- `get_drawing_dimensions` 已经返回基于行的读取模型:`dimensionType`、`viewId/viewType`、`orientation`、`direction`、`topDirection`、`referenceLine`、边界框集/段、`dimensionLine` 坐标和 `viewId/viewType`、`bbox` 集/段、`dimensionLine`/`leadLineMain`/`leadLineSecond`、`textBounds`
---
## 调试历史:这是如何工作的
开发文档 - 开发过程记录了问题和解决方案。
### 阶段 1. 初始架构
最初,该项目被设计为一个单一的 MCP 服务器,直接调用 Tekla API。
很快就发现:**这是不可能的**。
- MCP SDK (`ModelContextProtocol`) 需要 .NET 8+
- Tekla Structures 2021 Open API 仅适用于 **.NET Framework 4.8**
- 无法在单个进程中合并两个运行时 - 不同的运行时,不同的 CLR。
**解决方案**:分成两个进程。`TeklaMcpServer.exe`(net8)通过 `Process.Start` 启动 `TeklaBridge.exe`(net48),从 stdout 接收 JSON。
---
### 第 2 阶段。第一个关键错误 - MCP 下的 IPC 不起作用
编写 TeklaBridge 后,第一次测试显示出奇怪的现象:
- 从终端运行 `TeklaBridge.exe check_connection` → `{"status":"connected"}`
- 相同的二进制文件通过 MCP 服务器启动 → `RemotingException: Failed to connect to an IPC Port`
表面检查没有发现任何问题:进程在同一用户、同一会话和相同权限下运行(完整性级别没有区别)。
#### IPC 端口是什么意思?
Tekla Structures 使用 **.NET Remoting** - 基于命名管道的过时进程间通信机制。
客户端连接到命名管道,例如:
```
\\.\pipe\Tekla.Structures.Model-Console:2021.0.0.0
```
因此,如果名称与 Tekla 创建的名称不匹配,则连接是不可能的。
---
## 直接调用 TeklaBridge 而无需 Claude
TeklaBridge 支持 `--loop` 模式:从 stdin 读取 JSON 查询,在 stdout 中返回 JSON。
这允许直接从 PowerShell 测试命令:
```powershell
$bridge = "C:\TeklaStructures\2025.0\Environments\common\extensions\svMCP\TeklaBridge.exe"
'{"id":1,"cmd":"fit_views_to_sheet","args":[]}' | & $bridge --loop
```
请求格式:`{"id":<int>,"cmd":"<command>","args":[<arg1>,<arg2>,...]}`。
命令列表见 `TeklaBridge/Commands/DrawingCommandHandler.cs` 和 `ModelCommandHandler.cs`。
### 绘图打开的性能(批量)
`open_drawing` 阶段的性能分析发现瓶颈不是 `SetActiveDrawing`,而是 **查找绘图 GUID**:
| 阶段 | 时间 |
|---|---|
| 查找绘图(遍历所有绘图模型,每个绘图调用 `GetIdentifier().GUID`)| **~6.7 秒** |
| `SetActiveDrawing` 本身 | ~0.17 秒 |
**修复**:`TeklaDrawingQueryApi` 中的静态缓存 `Dictionary<Guid, Drawing>`(在 `--loop` 会话期间构建一次,失效 - `InvalidateDrawingCache()`)。查找 O(1)代替遍历 251 个绘图。
100 个 single-part 绘图(角尺寸标注)的批量结果:
| 模式 | 时间 |
|---|---|
| 视觉,无缓存 | ~20 分钟 |
| 背景(`showDrawing=false`),无缓存 | ~12 分钟 |
| **背景 + GUID 缓存** | **~68 秒** |
`showDrawing` 参数在 `open_drawing` 中(`args[2]`,默认为 `true`):`false` 在后台激活绘图而无需渲染 Drawing Editor。`SetActiveDrawing(drawing, false)` 比视觉模式 **~1 秒/绘图** 快得多,预热缓存的后台模式约为 **~0.2 秒/绘图**。
`place_contour_angle_dimensions` **幂等**:在放置前删除目标视图中现有的 `AngleDimension`,因此重复运行不会重复标注。
---
## 致谢
#### 通过反射进行诊断
为了理解客户端计算的通道名称,我们添加了诊断功能 - 通过反射从内部程序集中读取静态字段 `ChannelName`:
```csharp
var remoterType = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(a => { try { return a.GetTypes(); } catch { return Array.Empty<Type>(); } })
.FirstOrDefault(t => t.Name == "Remoter" && t.Namespace?.Contains("ModelInternal") == true);
var channelField = remoterType?.GetField("ChannelName",
BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
var channelName = channelField?.GetValue(null)?.ToString();
// 结果来自 MCP: "Tekla.Structures.Model-:2021.0.0.0"
// 应该是: "Tekla.Structures.Model-Console:2021.0.0.0"
```
**发现**: 通道名称中缺少 `Console` 一词。服务器监听 `...-Console:...`,而客户端尝试连接到 `...-:...`。
#### 原因
在 Tekla 源代码中(通过 dotPeek/ILSpy),我们找到了计算名称的逻辑:
```csharp
// 简化 - 在 Tekla.Structures.ModelInternal 中
static Remoter() {
var stdoutHandle = GetStdHandle(STD_OUTPUT_HANDLE); // WinAPI
var handleType = GetFileType(stdoutHandle);
// FILE_TYPE_CHAR (0x0002) = 控制台
// FILE_TYPE_PIPE (0x0003) = 管道
var suffix = (handleType == FILE_TYPE_CHAR) ? "Console" : "";
ChannelName = $"Tekla.Structures.Model-{suffix}:{version}";
}
```
也就是说,Tekla **故意** 根据进程的 stdout 更改通道名称。当 stdout 重定向到管道(MCP 服务器捕获 JSON 输出时),不添加 `-Console` 后缀。但是 **Tekla Structures 服务器始终创建带有 `-Console` 后缀的通道**,因为它的 stdout = 控制台。
结果:客户端搜索不存在的管道,连接断开。
#### 尝试绕过:直接创建文件
我们检查了:`CreateFile` 在 `\\.\pipe\Tekla.Structures.Model-Console:2021.0.0.0` 上执行成功,从两个上下文(终端和 MCP)都是如此。也就是说,管道存在且可访问。问题仅在于客户端库计算的通道名称不正确。
#### 第一个修复 - 替换一个字段
```csharp
// 手动加载 ModelInternal(它不会自动加载)
var modelInternalPath = Path.Combine(bridgeDir, "Tekla.Structures.ModelInternal.dll");
Assembly.LoadFrom(modelInternalPath);
// 查找类 Remoter 并修复 ChannelName
var remoter = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(a => { try { return a.GetTypes(); } catch { return Array.Empty<Type>(); } })
.First(t => t.Name == "Remoter" && t.Namespace?.Contains("ModelInternal") == true);
var field = remoter.GetField("ChannelName", BindingFlags.Static | BindingFlags.NonPublic);
field.SetValue(null, "Tekla.Structures.Model-Console:2021.0.0.0");
```
结果:**Model 已连接**。`check_connection` 工作正常。 🎉
---
### 阶段 3. 绘图不工作 - Drawing IPC 也损坏
在 Model 成功后,我们添加了 Drawing API 工具。测试显示:
- `check_connection`, `get_selected_elements_properties` 工作正常 ✅
- `list_drawings` → `RemotingException: Failed to connect to an IPC Port` ❌
**原因**: Tekla Drawing API 有 **单独的 IPC 通道** 在单独的内部程序集中。
三个子系统的通道:
- `ModelInternal.dll` → `Tekla.Structures.Model-Console:2021.0.0.0`
- `DrawingInternal.dll` → `Tekla.Structures.Drawing-Console:2021.0.0.0`
- `TeklaStructuresInternal.dll` → `Tekla.Structures.TeklaStructures-Console:2021.0.0.0`
第一个修复程序只修复了 `ModelInternal`。调用 Drawing API 时,会加载 `DrawingInternal.dll` 以及其损坏的通道。
#### 尝试:显式加载 DrawingInternal
```csharp
Assembly.LoadFrom(Path.Combine(dir, "Tekla.Structures.DrawingInternal.dll"));
// + 重复查找和替换 Drawing Remoter
```
它适用于 Drawing。但是现在 PDF 导出(`export_drawings_to_pdf`)仍然失败 - 这次是通过 `TeklaStructuresInternal`。
---
### 阶段 4. 最终修复 - 扫描所有 Internal 程序集的模式值
我们没有明确列出每个 `*Internal.dll`,而是创建了一个通用算法:
1. 强制加载所有 `Tekla.Structures.*Internal*.dll` 从 TeklaBridge 目录
2. 扫描所有加载的 Tekla 程序集中的所有静态字符串字段
3. 任何值 `Tekla.Structures.*-:*` (即有 `-:` - 空后缀)→ 替换 `-:` 为 `-Console:`
```csharp
// 1. 触摸公共程序集使其加载
_ = typeof(Tekla.Structures.Model.Model);
_ = typeof(Tekla.Structures.Drawing.DrawingHandler);
// 2. 强制加载 Internal 程序集 - 它们不会自动加载
var dir = Path.GetDirectoryName(typeof(DrawingHandler).Assembly.Location) ?? "";
foreach (var dll in Directory.GetFiles(dir, "Tekla.Structures.*Internal*.dll"))
try { Assembly.LoadFrom(dll); } catch { }
// 3. 扫描所有字段,修复值
var bindingFlags = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) {
if (!asm.GetName().Name.StartsWith("Tekla.Structures")) continue;
Type[] types; try { types = asm.GetTypes(); } catch { continue; }
foreach (var t in types)
foreach (var f in t.GetFields(bindingFlags)) {
if (f.FieldType != typeof(string)) continue;
try {
var val = f.GetValue(null)?.ToString() ?? "";
if (val.StartsWith("Tekla.Structures.") && val.Contains("-:"))
f.SetValue(null, val.Replace("-:", "-Console:"));
} catch { }
}
}
```
**关键细节:**
- 我们按值检查,而不是按类/字段名称 - 内部类名称可能在 Tekla 版本之间更改
- `GetTypes()` 用 try/catch 包装 - 一些程序集包含无法在没有依赖项的情况下解析的类型
- 在 .NET Framework 4.8 中,可以通过反射重写 readonly static 字段(在 .NET 8 中,由于 RVA 字段,这不再起作用)
- 修复程序必须在创建 `new Model()` 或 `new DrawingHandler()` 之前执行 - 静态类构造函数仅在首次访问时执行一次
`C:\temp\tekla_channel.txt` 中的最终结果:
```
Fixed 3 channel(s):
FIXED Tekla.Structures.ModelInternal.Remoter.ChannelName: Tekla.Structures.Model-:2021.0.0.0 -> Tekla.Structures.Model-Console:2021.0.0.0
FIXED Tekla.Structures.DrawingInternal.Remoter.ChannelName: Tekla.Structures.Drawing-:2021.0.0.0 -> Tekla.Structures.Drawing-Console:2021.0.0.0
FIXED Tekla.Structures.TeklaStructuresInternal.Remoter.ChannelName: Tekla.Structures.TeklaStructures-:2021.0.0.0 -> Tekla.Structures.TeklaStructures-Console:2021.0.0.0
```
---
### 阶段 5. 副作用:Tekla 写入 Console.Out
在第一次测试中,TeklaBridge 的 JSON 输出被 Tekla 的内部诊断污染:
```
[TeklaDebug] Connecting to IPC...
[TeklaDebug] Channel resolved: ...
{"status":"connected","modelName":"..."}
```
MCP 服务器无法解析响应 - JSON 不是第一行。
**解决方案**: 在调用 Tekla API 之前,我们捕获 `Console.Out`:
```csharp
var realOut = Console.Out;
var teklaLog = new StringWriter();
Console.SetOut(teklaLog); // Tekla 将写入此处
// ... 使用 Tekla API ...
// 我们的输出仅通过 realOut
realOut.WriteLine(JsonSerializer.Serialize(result));
// Tekla 诊断在出错时可用
var diag = teklaLog.ToString().Trim();
```
---
### 阶段 6. 部署问题
#### EXE 被 Claude Desktop 阻止
Claude Desktop 始终保持 `TeklaMcpServer.exe` 打开。在尝试重新编译时:
```
error MSB3021: Unable to copy file ... Access to the path is denied.
```
**解决方案**: 始终在 `dotnet build` 之前关闭 Claude Desktop。如果仅更改了 TeklaBridge,可以在不关闭的情况下热编译,因为 `bridge/TeklaBridge.exe` 未被阻止。
#### git 回滚后 NuGet 恢复
在 `git checkout` 到较早的提交后出现错误:
```
NETSDK1004: Assets file 'project.assets.json' not found.
Run a NuGet restore to generate this file.
```
**原因**: `obj/` 已添加到 `.gitignore` 并从 git 中删除。在签出后,`project.assets.json` 不存在。
**解决方案**:
```bash
dotnet restore src/TeklaBridge/TeklaBridge.csproj
dotnet build ...
```
#### 重构后文件冲突
在从单一 `ModelTools.cs` 转换为子目录中的部分类时,旧文件仍保留在磁盘上(未通过 git 删除)。编译器抱怨类重复。
**解决方案**: 显式删除 `rm src/TeklaMcpServer/ModelTools.cs` 在 git 恢复后。
---
### 阶段 7. 使用宏创建 GA 图纸
`create_general_arrangement_drawing` 仅作为临时解决方法保存。对于绘图块的进一步开发,我们使用 Open API 而不是宏。
历史上,GA 使用宏方法,因为它复制了选择保存的模型视图的 UI 工作流。这是 **不推荐的** 开发路径,因为它依赖于 UI、本地化和对话框元素的内部名称。
算法:
1. 通过 `TeklaStructuresSettings.GetAdvancedOption` 获取 `XS_MACRO_DIRECTORY`
2. 将临时 `.cs` 文件与宏写入 `modeling/` 子目录
3. 调用 `Operation.RunMacro(macroName)` 并通过 `IsMacroRunning()` 等待完成
4. 删除临时文件
细节:
- 文件名应无路径,仅 `macroname.cs`,目录自动确定
- `modeling/` 中的宏仅在建模模式下可用,而 `drawings/` 中的宏仅在图纸编辑器中可用
- `RunMacro` 是异步的,需要使用 `IsMacroRunning()` 进行轮询,带有超时
---
### 阶段 9. 支持 Tekla Structures 2025
在迁移到 TS2025 后,TeklaBridge 停止连接并出现新的错误:
```
RemotingIOException: Cannot connect to remoting service
'Tekla.Structures.Model-TeklaStructures-Console:2025.0.0.0' because it does not exist
```
#### TS2025 中的传输更改
TS2025 将 `.NET Remoting` (命名管道)替换为 `Trimble.Remoting` 位于 **Memory Mapped Files** (MMF)之上。
MMF 对象名称包括 DLL 的 **FileVersion** 而不是 AssemblyVersion。
| | TS2021 | TS2025 |
|---|---|---|
| 传输 | `System.Runtime.Remoting`,命名管道 | `Trimble.Remoting`,MMF |
| 通道名称 | `Tekla.Structures.Model-Console:2021.0.0.0` | `Tekla.Structures.Model-TeklaStructures-Console:2025.0.52577.0` |
| NuGet 版本 | `2021.0.0.0` = FileVersion | `2025.0.0.0` ≠ FileVersion `2025.0.52577.0` |
#### 尝试 1:通过反射进行运行时补丁
使用 `FileVersionInfo.GetVersionInfo()` 读取安装的 DLL 的 FileVersion 并更新加载的 NuGet 程序集中的静态 `ChannelName` 字段。 **未成功** - 静态构造函数在补丁时尚未执行;`Remoter.ChannelName` 为 `null`。
#### 解决方案:从包含 exe.config 的扩展文件夹启动
正确的方法是加载 **已安装的** Tekla DLL(具有正确的 FileVersion)而不是 NuGet 副本。
1. **TeklaBridge.exe** 复制到:
`C:\TeklaStructures\2025.0\Environments\common\extensions\svMCP\`
2. 旁边放置 **`TeklaBridge.exe.config`** 包含所有 Tekla/Trimble DLL 的 `<codeBase>` 记录:
```xml
<dependentAssembly>
<assemblyIdentity name="Tekla.Structures.Model" publicKeyToken="..." culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-2025.0.0.0" newVersion="2025.0.0.0"/>
<codeBase version="2025.0.0.0" href="file:///C:/TeklaStructures/2025.0/bin/Tekla.Structures.Model.dll"/>
</dependentAssembly>
```
3. 当 TeklaBridge 从该文件夹启动时,CLR 捕获 `exe.config` 并从 `C:\TeklaStructures\2025.0\bin\` 加载 DLL - FileVersion 为 `2025.0.52577.0`,通道有效。
4. **TeklaMcpServer** 在存在时自动从扩展文件夹选择桥接器:
```csharp
private static string ResolveBridgePath() {
// 新的 TS >= 2025 优先级最高,TeklaBridge.exe 在扩展文件夹中
var extensionsBridge = Directory.GetDirectories(@"C:\TeklaStructures")
.Where(d => Version.TryParse(...) && v.Major >= 2025)
.OrderByDescending(...)
.Select(d => Path.Combine(d, "Environments", "common", "extensions", "svMCP", "TeklaBridge.exe"))
.FirstOrDefault(File.Exists);
return extensionsBridge ?? localBridgePath;
}
```
#### 附加更改(TeklaBridge/Program.cs)
- API 版本定义:`typeof(Model).Assembly.GetName().Version?.Major`
- `< 2025` → `ApplyIpcChannelFix()` (旧反射修复 `-:` → `-Console:`)
- `>= 2025` → `ApplyTs2025ChannelVersionFix()` (读取 FileVersion,如果字段初始化则补丁)
- Bin 路径定义:TS2021 - `nt/bin/`,TS2025 - 直接 `bin/`
- TS2025 的 `XS_DIR` = 根目录(`C:\TeklaStructures\2025.0`),无 `nt/` 子目录
#### 结果
| 问题 | 原因 | 解决方案 |
|---|---|---|
| MMF 通道未找到 | NuGet DLL 的 FileVersion `2025.0.0.0` ≠ 已安装的 `2025.0.52577.0` | 从扩展文件夹启动,使用 `<codeBase>` 加载已安装的 DLL |
| 程序集找不到 TS2025 的路径 | 代码仅检查 `nt/bin/` | 添加直接检查 `bin/` |
| `XS_SYSTEM` 指向 TS2021 | 没有 - `DetectTeklaRootForMajor` 返回 null | 修复目录检查 |
---
### 阶段 8. filter_model_objects_by_type: 两个隐藏的错误
在实现过滤器工具后,该工具返回 `count: 0`,适用于所有类型,尽管模型中存在对象。
#### 错误 1:`is Beam` 在 .NET Remoting 代理中不起作用
`GetAllObjects()` 通过 IPC 以 **透明代理** 返回对象。`is` 操作符检查类型标识符相对于本地程序集 - 对于代理,它始终为 `false`,即使 `GetType().Name == "Beam"`。
```csharp
// 之前(对于 IPC 对象始终为 false):
modelObject is Beam
// 变成:
modelObject.GetType().Name.Equals("Beam", StringComparison.OrdinalIgnoreCase)
```
显示诊断:`GetType().Name` 正确返回 `"Beam"`,因此类型正确 - 但 `is` 操作符不识别它。这是 .NET Remoting 的特定行为:代理对象不是本地 `Beam` 的实例,尽管其行为与之相同。
#### 错误 2:`"count"` vs `"Count"` - JSON 属性名称区分大小写
`System.Text.Json` 以 **PascalCase** 序列化属性(到 CamelCase)。
```csharp
// FilteredModelObjectsResult 序列化为:
{ "Count": 59, "ObjectIds": [...] }
// 包装器查找(camelCase):
doc.RootElement.TryGetProperty("count", ...) // 始终 false → count = 0
// 修复:
doc.RootElement.TryGetProperty("Count", ...) || doc.RootElement.TryGetProperty("count", ...)
```
为什么以前未被注意到:`select_by_class` 和 `get_selected_weight` 串联化匿名 C# 对象 (`new { count }`) - 它们的字段以小写字母序列化并与预期键匹配。 `FilteredModelObjectsResult` 作为命名类 - 没有。
#### 附加信息:AutoFetch 和 foreach
`GetObjectsByFilter` 返回 `ModelObjectEnumerator`,它实现了 `IEnumerable`,但在 .NET Remoting 上 `foreach` 不可靠。将其替换为显式 `MoveNext()`。还添加了 `ModelObjectEnumerator.AutoFetch = false` - 没有它,在外部进程上下文中,对象的后台预加载可能会被切断。
MCP 配置
以下是此 MCP Server 的配置信息,可直接复制到 Cursor 或其他 MCP 客户端中使用。
mcp.json
连接信息
猜你喜欢
暂无推荐服务器