Content
# Tool List
## svMCP — Tekla Structures MCP Server
MCP (Model Context Protocol) server for working with **Tekla Structures 2021 and 2025** through Claude Desktop and other MCP clients.
## Requirements
- Windows 10/11
- .NET 8 SDK
- .NET Framework 4.8 (included in Windows 10+)
- Tekla Structures **2021** or **2025** (installed and running)
## Installation and Configuration
### 1. Build
```bash
dotnet build src/TeklaMcpServer/TeklaMcpServer.csproj -c Release
```
Automatically builds `TeklaMcpServer` (net8.0) + `TeklaBridge` (net48) and copies TeklaBridge.exe to `bin/Release/net8.0-windows/bridge/`.
### 2. Configure 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. Deploy after changes
```bash
# 1. Close Claude Desktop
# 2. Build
dotnet build src/TeklaMcpServer/TeklaMcpServer.csproj -c Release
# 3. Open Claude Desktop
```
> **Important:** TeklaMcpServer.exe is blocked while Claude Desktop is open.
> If only TeklaBridge was changed, it can be rebuilt without closing Claude Desktop,
> and the result is copied to `bridge/` automatically through the `BuildAndCopyTeklaBridge` target.
### Additional for Tekla Structures 2025
TeklaBridge must be launched from the Tekla extensions folder, not from the standard `bridge/` directory.
This is necessary because Tekla generates `exe.config` with `<codeBase>` entries that
point to installed DLLs with correct versions (FileVersion `2025.0.52577.0`, not NuGet `2025.0.0.0`).
Extensions folder: `C:\TeklaStructures\2025.0\Environments\common\extensions\svMCP\`
**Deploy TeklaBridge for TS2025:**
```bash
# Build TeklaBridge (without closing Claude Desktop)
dotnet build src/TeklaBridge/TeklaBridge.csproj -c Release
# Copy TeklaBridge.exe and TeklaMcpServer.Api.dll to the extensions folder
$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
# Also copy third-party dependencies (System.Text.Json, Newtonsoft.Json, etc.)
```
**exe.config:** created manually once and stored in the extensions folder.
Contains `<bindingRedirect>` + `<codeBase>` for all Tekla DLLs, pointing to `C:\TeklaStructures\2025.0\bin\`.
Source generator: `C:\temp\GenConfig2.ps1`.
TeklaMcpServer automatically determines the presence of TS2025 and launches the bridge from the extensions folder,
if the file exists (`ResolveBridgePath()` in `ModelTools.Shared.cs`).
## Architecture
```
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
```
### Why two processes?
Tekla Structures Open API requires **net48**.
MCP SDK requires **net8+**. Cannot combine in one process — different runtimes, different CLR.
| Version | IPC mechanism | Launch feature |
|---|---|---|
| TS2021 | .NET Remoting, named pipes | Reflection fix of channel name (suffix `-Console:`) |
| TS2025 | Trimble.Remoting, Memory Mapped Files | Launch from extensions folder with `exe.config` + `<codeBase>` |
TeklaBridge — thin net48 wrapper: accepts command as first argument, executes Tekla API, returns JSON to stdout.
## Project Structure
```
src/
├── TeklaMcpServer.Api/ # All Tekla API code (net48) — interfaces, DTOs, implementations
│ ├── Selection/ # IModelSelectionApi, ModelObjectInfo, TeklaModelSelectionApi
│ │ # ISelectionCacheManager, SelectionCacheManager
│ │ # SelectionResult, ToolInputSelectionHandler
│ ├── Drawing/ # Common drawing layer: DrawingContext, DrawingViewContext,
│ │ # MarksViewContext, API/DTOs/builders for views/marks/dimensions
│ │ # DrawingViewInfo, DrawingViewsResult, DrawingMarkInfo, …
│ ├── Algorithms/
│ │ ├── Packing/ # MaxRectsBinPacker
│ │ └── Marks/ # MarkLayoutEngine, candidate generation, scoring, overlap resolver
│ └── Filtering/
│ ├── Common/ # FilterExpressionParser, FilterTokenizer, FilterAstBuilder, FilterHelper…
│ ├── Drawing/ # DrawingObjectsFilterHelper
│ └── Model/ # IModelFilteringApi, DTOs, TeklaModelFilteringApi
├── TeklaMcpServer/ # MCP server (net8.0-windows)
│ ├── Program.cs # Entry point, MCP host configuration
│ └── Tools/ # Thin MCP wrappers
│ ├── Shared/ # RunBridge(), common code
│ ├── Connection/ # check_connection
│ ├── Model/ # Model tools
│ └── Drawing/ # Drawing tools
├── TeklaBridge/ # Bridge process (net48) — only command dispatcher
│ ├── Program.cs # Entry point + IPC fix + Console capture
│ └── Commands/
│ ├── ModelCommandHandler.cs
│ └── DrawingCommandHandler.cs
└── svMCP/ # Stub (not used)
```
### Responsibility Separation
| Layer | Project | Role |
|---|---|---|
| MCP Tools | `TeklaMcpServer/Tools/` | **Only thin wrappers** — call `RunBridge()`, JSON parsing, string return |
| Bridge dispatcher | `TeklaBridge/Commands/` | Command dispatch to `TeklaMcpServer.Api` classes, result serialization |
| All Tekla code | `TeklaMcpServer.Api/` | Interfaces, DTOs, and all Tekla API implementations |
`TeklaMcpServer.Api` — separate net48 project (not shared with net8 server), its value lies in clear contract boundaries within the bridge process: overview, testability through mock, and readiness to support multiple Tekla versions.
## Available Tools
### Connection
| Tool | Description |
|---|---|
| `check_connection` | Check connection to Tekla Structures; return model name and path |
### Model
| Tool | Description |
|---|---|
| `get_selected_elements_properties` | Properties of selected elements: Part, BoltGroup, Weld, RebarGroup — all types |
| `get_selected_elements_total_weight` | Total weight of selected elements (kg) |
| `select_elements_by_class` | Select elements by Tekla class number |
| `filter_model_objects_by_type` | Find and select model objects by type (beam, plate, bolt, assembly…) |
Drawings
> Requires an open Drawing Editor in Tekla Structures
| Tool | Description |
|---|---|
| `list_drawings` | List of all model drawings |
| `find_drawings` | Search by name and/or mark (contains, case-insensitive) |
| `find_drawings_by_properties` | Search by multiple properties (JSON filters) |
| `open_drawing` | Open (activate) a drawing by GUID |
| `close_drawing` | Close the active drawing |
| `export_drawings_to_pdf` | Export drawings to PDF by GUID |
| `create_general_arrangement_drawing` | Legacy workaround: create a GA drawing from a saved model view via a macro |
| `create_single_part_drawing` | Create a Single Part drawing via Tekla Open API |
| `create_assembly_drawing` | Create an Assembly drawing via Tekla Open API |
| `get_d` | Active drawing and selected objects |
| `get_drawing_layout_context` | Coarse `DrawingContext`: drawing/sheet/views/reserved layout |
| `get_drawing_view_context` | Detailed `DrawingViewContext` for a single view |
| `get_sheet_objects_debug` | Dev/debug: all sheet objects, their bbox, and candidate reserved areas |
| `select_drawing_objects` | Select drawing objects by model object ID |
| `filter_drawing_objects` | Filter drawing objects by type (Mark, Part, DimensionBase…) |
| `set_mark_content` | Change mark content and font |
| `get_drawing_views` | All views of drawing: position, scale, size, sheet sizes |
| `get_drawing_reserved_areas` | Reserved areas of the sheet: tables, margins, and final `get_drawing_section_sides` | Section view the base view |
| `move_view` | Move the sheet (absolutely or by offset) |
| `set_view_scale` | Change the scale of one or more views |
| `fit_views_to_sheet` automatic view arrangement path: standard scale selection, orthographic layout without overlaps, and post-alignment by projection connection |
| `get_drawing_marks` | Read marks: position, bbox/OBB, `resolvedGeometry`, overlaps, leader lines, PropertyElement content; filtering by view |
| `create_part_marks` | Create part marks with specified content and style |
| `delete_all_marks` | Delete all marks on the active drawing |
| `get_drawing_parts` | All model objects of the drawing: PART_POS, ASSEMBLY_POS, PROFILE, MATERIAL, NAME |
| `get_drawing_dimensions` | All `StraightDimensionSet` of the active drawing: id, `dimensionType`, distance, `viewId/viewType`, orientation, `direction`, `topDirection`, `referenceLine`, bbox set/segments, `dimensionLine`, `leadLineMain/Second`, `textBounds` |
| `get_dimension_contexts` | Internal/context read path for dimensions by view |
| `arrange_dimensions` | Deterministic spacing of parallel dimension `Distance` |
| `combine_dimensions` | Controlled combine path for compatible dimension sets |
| `move_dimension` | Move a dimension line by delta (changes `StraightDimensionSet.Distance`) |
| `create_dimension` | Create a `StraightDimensionSet` by a set of points |
| `delete_dimension` | Delete a `StraightDimensionSet` by ID |
| `place_control_diagonals` | Control diagonal dimensions by the actual solid geometry of visible parts in the view; filtering by `MATERIAL_TYPE` (default: steel/concrete/wood, without insulation); direction is always from bottom to top |
| `get_part_geometry_in_view` partbbox, start/end, axes) in the local |
| `get_all_parts_geometry_in_view` | Batch get geometry of all parts in the view in one call |
| `get_grid_axes` | Get grid axes in a specified drawing view |
| `resolvelaps` | Automatically block overlaps within each view local shifts |
| `arrange_marks` | Full automatic mark arrangement within each view around the anchor point |
| `arrange_marks_no_collisions` | Combined pass: `arrange_marks` + repeated `resolve_mark_overlaps` until stabilization |
| `draw_debug_overlay` | Dev/debug: draw a temporary overlay (line/polygon/text/cross) in the drawing |
| `clear_debug_overlay` | Dev/debug: clear the overlay layer entirely or by group |
| `draw_selected_mark_part_axis_geometry` | Dev/debug: show part axis and geometry for selected marks |
Mark layout is currently implemented as follows:
- The unit of processing is one `View`, not the entire drawing sheet.
- The factual mark layer is now implemented as `MarksViewContext` + `MarksViewContextBuilder`.
- `get_drawing` is already built on top of this context layer but retains the previous external DTO `DrawingMarkInfo`.
- All layout engine computations are performed in the local coordinate system of the view.
- The Tekla layer only collects neutral `MarkLayoutItem` and translates offsets between view and sheet coordinates.
- `resolve_mark_overlaps` uses only the local `MarkOverlapResolver` for minimal shifts within the view.
- `arrange_marks` uses `MarkLayoutEngine`: candidate generation, scoring, greedy placement, and then local overlap resolver.
--line marks, the anchor is taken from `LeaderLinePlacing.StartPoint` in view coordinates; `StartPoint` remains unchanged, only `InsertionPoint` is moved.
- Mark geometry is centralized in `MarkGeometryHelper` and depends on `PlacingType`:
- `LeaderLinePlacing` — object-aligned geometry of the mark itself
- `BaseLinePlacing` / `AlongLinePlacing` — direction from the associated part in the current view
- Fallback path — raw Tekla geometry, but it is not considered canonical for collision/layout reasoning.
The drawing layer contexts are currently implemented as follows:
- `DrawingContext` — coarse sheet-level context for layout views, reserved areas, and `before/after` cases.
- `DrawingViewContext` — detailed per-view geometry context.
- `MarksViewContext` — factual mark context of a single view.
- Dimensions go in the same direction through `get_dimension_contexts`.
View layout is currently implemented as follows:
- Legacy `PlaceViews()` has been removed from the MCP/bridge layer and is no longer a supported tool.
- The main automatic view arrangement path is `fit_views_to_sheet`.
- Sheet-level reasoning now relies on `DrawingContext` (`get_drawing_layout_context`).
- Post-processing of projection connections is performed within `DrawingProjectionAlignmentService`.
- Reserved tables: `DrawingReservedAreaReader.ReadLayoutTableGe uses `TableLayout.GetCurrentTables()` → `PresentationConnection.GetObjectPresentation()` → canvasSegment.Primitives[0/2]`) → precise bbox. Tables with `OverlapWithViews=true` are skipped Sheet margins: `TableMarginsAndSpaces()` margins (usually 5–10mm), used asMargin` in the response.
- The canonical source of layout table boundaries is precisely the canvas markers `Segment.Primitives[2]`; general accumulation of primitives is only allowed as.
Dimensions are currently implemented as follows:
- `get_dimensions` already returns-based read model: `dimensionType`, `viewId/viewType`, `orientation`, `direction`, `topDirection`,Line`, bbox set/segments, and `dimensionleadLineMain/leadLineSecond`.
- `get_dimension_contexts` provides a path on top of the same drawing/view-level layer.
- `arrange_dimensions` already works but is not a full layout engine; it is a conservative deterministic spacing path for parallel stacks.
- `combine_dimensions` already works as a controlled combine path and is not identical to regular grouping.
- `TextBounds` currently remains `null` until Tekla-side text geometry is confirmed at runtime.
- The `Drawing/Dimensions` block is currently being redesigned according to the standard `D:\repos\svMCP\dim`.
- Public `arrange_dimensions` and `get_dimension_arrangement_debug` are temporarily hidden until the line-based redesign is completed.
## Diagnostics
| File | Content |
|---|---|
| `C:\temp\teklabridge_log.txt` | Details of the last error (JSON) |
| `C:\temp\tekla_channel.txt` | Result of fixing IPC channel names (number of channels fixed) |
| `%TEMP%\svmcp-perf.log` | Profiling by layers `mcp/transport/bridge/api` |
Profiling:
- `SVMCP_PERF=1` — enable timing recording.
- `SVMCP_PERF_LOG=<path>` — path to the log file (default: `%TEMP%\svmcp-perf.log`)
### Direct TeklaBridge Call without Claude
TeklaBridge supports the `--loop` mode: reads JSON requests from stdin, returns JSON to stdout. This allows testing commands directly from PowerShell:
```powershell
$bridge = "C:\TeklaStructures\2025.0\Environments\common\extensions\svMCP\TeklaBridge.exe"
'{"id":1,"cmd":"fit_views_to_sheet","args":[]}' | & $bridge --loop
```
Request format: `{"id":<int>,"cmd":"<command>","args":[<arg1>,<arg2>,...]}`.
List of commands — see `TeklaBridge/Commands/DrawingCommandHandler.cs` and `ModelCommandHandler.cs`.
### Performance of Opening Drawings (Batch)
Profiling `open_drawing` by phases revealed that the bottleneck was not `SetActiveDrawing`, but **searching for a drawing by GUID**:
| Phase | Time |
|---|---|
| searching for a drawing (iterating over all model drawings, IPC call `GetIdentifier().GUID` for each) | **~6.7 s** |
| `SetActiveDrawing` itself | ~0.17 s |
**Fix:** static cache `Dictionary<Guid, Drawing>` in `TeklaDrawingQueryApi` (built once per `--loop` session, invalidation via `InvalidateDrawingCache()`). Search O(1) instead of iterating over 251 drawings.
Result on a batch of 100 single-part drawings (placing corner dimensions):
| Mode | Time |
|---|---|
| Visual, without cache | ~20 min |
| Background (`showDrawing=false`), without cache | ~12 min |
| **Background + GUID cache** | **~68 s** |
The `showDrawing` parameter in `open_drawing` (`args[2]`, default `true`): `false` activates the drawing in the background without rendering Drawing Editor. `SetActiveDrawing(drawing, false)` is noticeably faster for batch (visible mode ~1 s/drawing, background with warmed-up cache ~0.2 s/drawing).
`place_contour_angle_dimensions` is **idempotent**: before placing, it removes existing `AngleDimension` in the target view, so a repeated run does not duplicate dimensions.
---
## Debugging History: How It All Worked
Documentation of the development process - what went wrong and how solutions were found.
### Stage 1. Initial Architecture
Initially, the project was conceived as a single MCP server with a direct call to the Tekla API.
It quickly became clear that **this is impossible**.
- MCP SDK (`ModelContextProtocol`) requires .NET 8+
- Tekla Structures 2021 Open API is only provided for **.NET Framework 4.8**
- Two runtimes in one process are incompatible
**Solution**: split into two processes. `TeklaMcpServer.exe` (net8) launches `TeklaBridge.exe` (net48) via `Process.Start`, receives JSON from stdout.
---
### Stage 2. First Critical Bug - IPC Doesn't Work from MCP
After writing TeklaBridge, the first tests showed a strange picture:
- `TeklaBridge.exe check_connection` from the terminal → `{"status":"connected"}` ✅
- The same binary, launched through the MCP server → `RemotingException: Failed to connect to an IPC Port` ❌
A superficial check revealed nothing: the process is launched under the same user, in the same session, with the same rights (integrity level did not differ).
#### What Does "IPC Port" Mean?
Tekla Structures uses **.NET Remoting** - an outdated inter-process communication mechanism based on named pipes. The client connects to a named pipe of the form:
```
\\.\pipe\Tekla.Structures.Model-Console:2021.0.0.0
```
Accordingly, if the name does not match the one created by the server (Tekla Structures itself), the connection is impossible.
#### Diagnostics via Reflection
To understand what channel name the client calculates, we added diagnostics - via reflection, we read the static field `ChannelName` from the internal assembly:
```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();
// Result from MCP: "Tekla.Structures.Model-:2021.0.0.0"
// Should be: "Tekla.Structures.Model-Console:2021.0.0.0"
```
**Finding**: the word `Console` is missing in the channel name. The server listens to `...-Console:...`, the client tries to connect to `...-:...`.
#### Cause
After digging into the Tekla sources (via dotPeek/ILSpy), we found the logic for calculating the name:
```csharp
// Simplified - inside Tekla.Structures.ModelInternal
static Remoter() {
var stdoutHandle = GetStdHandle(STD_OUTPUT_HANDLE); // WinAPI
var handleType = GetFileType(stdoutHandle);
// FILE_TYPE_CHAR (0x0002) = console
// FILE_TYPE_PIPE (0x0003) = pipe
var suffix = (handleType == FILE_TYPE_CHAR) ? "Console" : "";
ChannelName = $"Tekla.Structures.Model-{suffix}:{version}";
}
```
That is, Tekla **intentionally** changes the channel name depending on where the process's stdout is directed. When stdout is redirected to a pipe (which MCP server does to capture JSON output), the `-Console` suffix is not added. But **Tekla Structures server creates a channel with the `-Console` suffix always**, because its stdout = console.
Result: the client searches for a non-existent pipe, and the connection drops.
#### Attempt to Workaround: CreateFile Directly
We checked: `CreateFile` on `\\.\pipe\Tekla.Structures.Model-Console:2021.0.0.0` - SUCCESS from both contexts (terminal and MCP). That is, the pipe exists and is accessible. The problem is exclusively in the incorrect name calculated by the client library.
#### First Fix Option - Replacing One Field
```csharp
// Load ModelInternal manually (it doesn't load automatically)
var modelInternalPath = Path.Combine(bridgeDir, "Tekla.Structures.ModelInternal.dll");
Assembly.LoadFrom(modelInternalPath);
// Find the Remoter class and fix 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");
```
Result: **Model connected**. `check_connection` worked. 🎉
---
### Stage 3. Drawings Don't Work - Drawing IPC Also Broken
After success with Model, we added Drawing API tools. Tests showed:
- `check_connection`, `get_selected_elements_properties` - work ✅
- `list_drawings` → `RemotingException: Failed to connect to an IPC Port` ❌
**Reason**: Tekla Drawing API has a **separate IPC channel** in a separate internal assembly.
Channels for three subsystems:
- `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`
The first fix only fixed `ModelInternal`. When calling Drawing API, `DrawingInternal.dll` is loaded with its own broken channel.
#### Attempt: Explicit Loading of DrawingInternal
```csharp
Assembly.LoadFrom(Path.Combine(dir, "Tekla.Structures.DrawingInternal.dll"));
// + repeat search-and-replace for Drawing Remoter
```
Worked for Drawing. But now PDF export (`export_drawings_to_pdf`) still crashed - already through `TeklaStructuresInternal`.
Stage 4. Final Fix — Scanning All Internal Assemblies by Value Pattern
Instead of explicitly listing each `*Internal.dll`, a universal algorithm was created:
1. Force-load **all** `Tekla.Structures.*Internal*.dll` from the TeklaBridge directory
2. Scan **all** static string fields in **all** loaded Tekla assemblies
3. Any value `Tekla.Structures.*-:*` (i.e., `-:` is an empty suffix) → replace `-:` with `-Console:`
```csharp
// 1. Touch public assemblies to load them
_ = typeof(Tekla.Structures.Model.Model);
_ = typeof(Tekla.Structures.Drawing.DrawingHandler);
// 2. Force-load Internal assemblies — they are not loaded automatically
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. Scan all fields, fix by value
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 { }
}
}
```
**Key nuances:**
- We check **by value**, not by class/field name — internal class names can change between Tekla versions
- `GetTypes()` is wrapped in try/catch — some assemblies contain types that do not resolve without dependencies
- In .NET Framework 4.8, you can overwrite readonly static fields via reflection (in .NET 8, this no longer works due to RVA fields)
- The fix must be performed **before** creating `new Model()` or `new DrawingHandler()` — static class constructors are executed once at first access
Result `C:\temp\tekla_channel.txt` after the final fix:
```
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
```
---
### Stage 5. Side Issue: Tekla Writes to Console.Out
During initial tests, the JSON output of TeklaBridge was "polluted" by Tekla's internal diagnostics:
```
[TeklaDebug] Connecting to IPC...
[TeklaDebug] Channel resolved: ...
{"status":"connected","modelName":"..."}
```
The MCP server could not parse the response — JSON was not the first line.
**Solution**: before the first access to the Tekla API, intercept `Console.Out`:
```csharp
var realOut = Console.Out;
var teklaLog = new StringWriter();
Console.SetOut(teklaLog); // Tekla writes here
// ... work with Tekla API ...
// Our output — only through realOut
realOut.WriteLine(JsonSerializer.Serialize(result));
// Tekla diagnostics are available on errors
var diag = teklaLog.ToString().Trim();
```
---
### Stage 6. Deployment Issues
#### EXE Blocked by Claude Desktop
Claude Desktop keeps `TeklaMcpServer.exe` open all the time it is running. When trying to rebuild:
```
error MSB3021: Unable to copy file ... Access to the path is denied.
```
**Solution**: always close Claude Desktop before `dotnet build`. If only TeklaBridge has changed, it can be rebuilt hot, because `bridge/TeklaBridge.exe` is not blocked.
#### NuGet Restore After Git Rollback
After `git checkout` to an earlier commit, an error appeared:
```
NETSDK1004: Assets file 'project.assets.json' not found.
Run a NuGet generate this file.
```
**Cause**: `obj/` was added to `.gitignore` and removed from git. After checkout, `project.assets.json` is missing.
**Solution**:
```bash
dotnet restoreeklaBridge/TeklaBridge.csproj
dotnet build ...
```
#### File Conflict After Refactoring
When switching from a monolithic `ModelTools.cs` to partial classes in subdirectories, the old file remained on disk (not removed via git). The compiler complained about duplicate classes.
**Solution**: explicitly delete `rm src/TeklaMcpServer/ModelTools.cs` after git restore.
---
### Stage 7. Legacy: Creating GA Drawings via Macro
`create_general_arrangement_drawing` is saved only as a temporary legacy workaround. For further development of the drawing creation block, we use Open API, not macros.
Historically, a macro approach was used here for GA because it repeated the standard UI workflow of choosing a saved model view. This is not a recommended path for further development: it depends on UI, localization, and internal names of dialog elements.
Algorithm:
1. Learn `XS_MACRO_DIRECTORY` via `TeklaStructuresSettings.GetAdvancedOption`
2. Write a temporary `.cs` file with a macro to a subdirectory `modeling/`
3. Call `Operation.RunMacro(macroName)` and wait for completion via `IsMacroRunning()`
4. Delete the temporary file
Nuances:
- The file name should be without a path — only `macroname.cs`, the directory is determined automatically
- Macros in `modeling/` are only available in modeling mode, in `drawings/` — in the drawing editor
- `RunMacro` is asynchronous, polling via `IsMacroRunning()` with a timeout is required
---
### Stage 9. Support for Tekla Structures 2025
After switching to TS2025, TeklaBridge stopped connecting with a new error:
```
IOException: Cannot connect to remoting service
'Tekla.Structures.Model-TeklaStructures-Console:2025.0.0.0' because it does not exist
```
#### Transport Change in TS2025
TS2025 replaced `.NET Remoting` (named pipes) with `Trimble.Remoting` over **Memory Mapped Files** (MMF).
The MMF object name includes not the AssemblyVersion, but the **FileVersion** of the installed DLL.
| | TS2021 | TS2025 |
|---|---|---|
| Transport | `Systemoting`, named pipes |ble.Remoting`, MM| Channel name |la.Structures.Model-Console:2021.0.0.0` | `Tekla.Structures.Model-TeklaStructures-Console:2025.0.52577.0` |
| NuGet version | `2021.0.0` = FileVersion2025.0.0.0` ≠ FileVersion5.0.52577.0` |
#### Attempt 1: Runtime Patch via Reflection
Read the FileVersion of the installed DLL via `FileVersionInfo.GetVersionInfo()` and update
static fields `ChannelName` in loaded NuGet assemblies. **Did not work**
constructors of DLLs had not yet executed at the time of patching; `Remoter.ChannelName `null`.
#### Solution: Run from Extensions Folder with exe.config
The correct approach is to load the **installed** Tekla DLLs (with the correct FileVersion) instead of NuGet copies.
1. **TeklaBridge.exe** is copied to:
`C:\TeklaStructures\2025.0\Environments\common\extensions\svMCP\`
2. A nearby **`TeklaBridge.exe.config`** is placed with `<codeBase>` entries for all Tekla/Trimble DLLs:
```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. When TeklaBridge runs from this folder, CLR picks up `exe.config` and loads
DLLs from `C:\TeklaStructures\2025.0\bin\` — there FileVersion `2025.0.52577.0`, the channel works.
4. **TeklaMcpServer** automatically selects the bridge from the extensions folder if available:
```csharp
private static string ResolveBridgePath() {
// Newest TS >= 2025 with TeklaBridge.exe in extensions folder takes priority
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;
}
```
#### Additional Changes (TeklaBridge/Program.cs)
- API version definition: `typeof(Model).Assembly.GetName().Version?.Major`
- `< 2025` → `ApplyIpcChannelFix()` (old reflection fix `-:` → `-Console:`)
- `>= 2025` → `ApplyTs2025ChannelVersionFix()` (reads FileVersion, patches if fields are initialized)
- Bin path definition: TS2021 — `nt/bin/`, TS2025 — `bin/` directly
- `XS_DIR` for TS2025 = root folder (`C:\TeklaStructures\2025.0`), without `nt/` subdirectory
#### Summary
| Issue | Cause | Solution |
|---|---|---|
| MMF channel not found | NuGet DLL has FileVersion `2025.0.0.0`, not `2025.0.52577.0` | Run from extensions folder, DLLs loaded via `<codeBase>` from installed dir |
| Build doesn't find TS2025 path | Code only checked `nt/bin/` | Added direct check of `bin/` |
| `XS_SYSTEM` pointed to TS2021 | No — `DetectTeklaRootForMajor` returned null | Fixed directory check |
---
### Summary (all versions)
| Issue | Cause | Solution |
|---|---|---|
| IPC doesn't work from pipe (TS2021) | Tekla calculates channel name based on stdout type | Reflection fix for all `*Internal` channels to `new Model()` |
| Drawing API doesn't connect | `DrawingInternal.dll` not loaded automatically | Force-load all `*Internal*.dll` |
| PDF export crashes | `TeklaStructuresInternal` also broken | Universal scanning by value `-:` |
| Garbage in JSON output | Tekla writes to Console.Out | Intercept Console.Out before API calls |
| EXE blocked | Claude Desktop holds file open | Close Claude Desktop before rebuilding |
| NETSDK1004 after git | `obj/` in .gitignore | `dotnet restore` before build |
| `filter_model_objects` always returned 0 | `is Beam` doesn't work for IPC proxy + `"count"` vs `"Count"` | `GetType().Name` + both key forms |
| MMF channel not found (TS2025) | FileVersion mismatch: NuGet `2025.0.0.0` ≠ installed `2025.0.52577.0` | extensions folder + exe.config with `<codeBase>` on installed DLLs |
---
### Step 8. filter_model_objects_by_type: two hidden bugs
After implementing filtering, the tool returned `count: 0` for any type, even though objects existed in the model.
#### Bug 1: `is Beam` doesn't work for .NET Remoting proxy
`GetAllObjects()` returns objects via IPC as **transparent proxy**. The `is` operator checks type identity relative to the local assembly — for proxies, it's always `false`, even if `GetType().Name == "Beam"`.
```csharp
// Was (always false for IPC objects):
modelObject is Beam
// Changed to:
modelObject.GetType().Name.Equals("Beam", StringComparison.OrdinalIgnoreCase)
```
Diagnostics showed: `GetType().Name` returns `"Beam"` correctly, so the type is correct — but the `is` operator doesn't recognize it. This is a .NET Remoting specificity: a proxy object isn't an instance of the local `Beam`, even though it behaves like it.
#### Bug 2: `"count"` vs `"Count"` — JSON property case
`System.Text.Json` serializes properties **by C# field name as-is** (PascalCase → `"Count"`).
```csharp
// FilteredModelObjectsResult serializes as:
{ "Count": 59, "ObjectIds": [...] }
// Wrapper looked for (camelCase):
doc.RootElement.TryGetProperty("count", ...) // always false → count = 0
// Fixed:
doc.RootElement.TryGetProperty("Count", ...) || doc.RootElement.TryGetProperty("count", ...)
```
Why it wasn't noticed earlier: `select_by_class` and `get_selected_weight` use anonymous C# objects (`new { count }`) — their fields serialize in lowercase and match the expected key. `FilteredModelObjectsResult` as a named class doesn't.
#### Additionally: AutoFetch and foreach
`GetObjectsByFilter` returns `ModelObjectEnumerator`, which implements `IEnumerable`, but `foreach` in .NET Remoting context works unreliably. Replaced with explicit `MoveNext()`. Also added `ModelObjectEnumerator.AutoFetch = false` — without it, in the context of an external process, background object fetching may be interrupted.
MCP Config
Below is the configuration for this MCP Server. You can copy it directly to Cursor or other MCP clients.
mcp.json
Connection Info
You Might Also Like
No recommendations available