@rui.branco/revit-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,676 @@
1
+ #Requires -Version 5.1
2
+ <#
3
+ .SYNOPSIS
4
+ Installs the Revit MCP Bridge add-in for every installed Revit 2025+.
5
+
6
+ .DESCRIPTION
7
+ One command, no Visual Studio, no admin rights, and no .NET SDK:
8
+
9
+ .\install.ps1
10
+
11
+ Detects Revit under Program Files, copies the add-in to
12
+ %APPDATA%\Autodesk\Revit\Addins\<version>\RevitMcpBridge\ and writes the .addin manifest
13
+ next to it. Re-running is safe: the install folder is replaced and the manifest overwritten.
14
+
15
+ By default the add-in comes from the prebuilt dist\ folder next to this script - that is
16
+ what the npm package ships, and it is why an end user needs neither the .NET SDK nor a
17
+ build step. The DLL there is compiled against the Revit 2025 reference assemblies and
18
+ loads in Revit 2025, 2026 and 2027 alike, so one binary covers every supported version.
19
+ Pass -Build to compile a fresh one from source instead; that is the contributor path and
20
+ the only one that needs the SDK.
21
+
22
+ Written for Windows PowerShell 5.1 as well as PowerShell 7 - deliberately no ternary
23
+ operator, no ??, no && / ||, and no -AsHashtable.
24
+
25
+ Every human-readable line is prefixed with "[revit-bridge] " and a fixed-width level so a
26
+ caller can grep it. Pass -Json to get a single JSON result object as the only stdout output
27
+ instead, which is the mode an MCP "install bridge" tool should use.
28
+
29
+ .PARAMETER RevitVersion
30
+ Restrict the operation to one version, e.g. 2026. Without it, every detected version is used.
31
+
32
+ .PARAMETER Build
33
+ Force a fresh dotnet build -c Release and install that output, ignoring dist\. Requires the
34
+ .NET SDK. For contributors working on the C# side.
35
+
36
+ .PARAMETER SkipBuild
37
+ Never build. Install the prebuilt dist\ folder, or whatever is already in bin\Release if
38
+ dist\ is absent. This is the default behaviour whenever dist\ is present, so the switch is
39
+ only meaningful in a source checkout with no dist\.
40
+
41
+ .PARAMETER Uninstall
42
+ Remove the manifest and the install folder instead of installing.
43
+
44
+ .PARAMETER Json
45
+ Emit one JSON result object as the only stdout output. Nothing else is printed.
46
+
47
+ .EXAMPLE
48
+ .\install.ps1
49
+ .EXAMPLE
50
+ .\install.ps1 -RevitVersion 2026
51
+ .EXAMPLE
52
+ .\install.ps1 -Build
53
+ .EXAMPLE
54
+ .\install.ps1 -Uninstall -Json
55
+ #>
56
+ [CmdletBinding()]
57
+ param(
58
+ [string] $RevitVersion,
59
+ [switch] $Build,
60
+ [switch] $SkipBuild,
61
+ [switch] $Uninstall,
62
+ [switch] $Json
63
+ )
64
+
65
+ $ErrorActionPreference = 'Stop'
66
+
67
+ # ---------------------------------------------------------------------------------------------
68
+ # Constants. The AddInId is fixed and must never change: Revit keys the add-in on this GUID, and
69
+ # a new one would leave a stale duplicate registered.
70
+ # ---------------------------------------------------------------------------------------------
71
+ $AddInName = 'RevitMcpBridge'
72
+ $AssemblyFileName = 'RevitMcpBridge.dll'
73
+ $FullClassName = 'RevitMcpBridge.BridgeApplication'
74
+ $AddInId = '8f2c7a41-3e6b-4d19-9c05-1b7a52e4d83f'
75
+ $VendorId = 'com.github.revit-mcp'
76
+ $VendorDescription = 'Revit MCP Bridge - in-process HTTP bridge for the Node MCP server'
77
+ $ManifestFileName = 'RevitMcpBridge.addin'
78
+ $MinimumRevitYear = 2025
79
+ $ProjectFileName = 'RevitMcpBridge.csproj'
80
+ $DistDirName = 'dist'
81
+
82
+ # Revit ships these itself. If one ever appears in the build output it must not be copied next to
83
+ # the add-in, or Revit loads two copies of the API and every type check fails.
84
+ $RevitOwnedFiles = @(
85
+ 'RevitAPI.dll',
86
+ 'RevitAPIUI.dll',
87
+ 'AdWindows.dll',
88
+ 'UIFramework.dll',
89
+ 'RevitAPI.xml',
90
+ 'RevitAPIUI.xml',
91
+ 'RevitAPI.runtimeconfig.json',
92
+ 'RevitAPIUI.runtimeconfig.json'
93
+ )
94
+
95
+ $ScriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
96
+
97
+ $script:Log = New-Object System.Collections.ArrayList
98
+ $script:Warnings = New-Object System.Collections.ArrayList
99
+ $script:Errors = New-Object System.Collections.ArrayList
100
+ $script:VersionResults = New-Object System.Collections.ArrayList
101
+ $script:ExitCode = 0
102
+ $script:BuildStatus = 'skipped'
103
+ $script:OutputDir = $null
104
+
105
+ # ---------------------------------------------------------------------------------------------
106
+ # Output helpers
107
+ #
108
+ # Writes straight to the process stdout handle rather than the PowerShell pipeline, so these can
109
+ # be called from inside a function without their text becoming that function's return value.
110
+ # ---------------------------------------------------------------------------------------------
111
+ function Write-Line {
112
+ param(
113
+ [string] $Level,
114
+ [string] $Message
115
+ )
116
+
117
+ $line = '[revit-bridge] ' + $Level.PadRight(9) + ' ' + $Message
118
+ $null = $script:Log.Add($line)
119
+
120
+ if (-not $Json) {
121
+ [Console]::Out.WriteLine($line)
122
+ }
123
+ }
124
+
125
+ function Write-Warn {
126
+ param([string] $Message)
127
+
128
+ $null = $script:Warnings.Add($Message)
129
+ Write-Line -Level 'WARN' -Message $Message
130
+ }
131
+
132
+ function Write-Err {
133
+ param([string] $Message)
134
+
135
+ $null = $script:Errors.Add($Message)
136
+ Write-Line -Level 'ERROR' -Message $Message
137
+ }
138
+
139
+ # ---------------------------------------------------------------------------------------------
140
+ # Detection
141
+ # ---------------------------------------------------------------------------------------------
142
+ function Get-AutodeskRoots {
143
+ $roots = New-Object System.Collections.ArrayList
144
+
145
+ $candidates = @($env:ProgramW6432, $env:ProgramFiles, 'C:\Program Files')
146
+
147
+ foreach ($candidate in $candidates) {
148
+ if ([string]::IsNullOrWhiteSpace($candidate)) {
149
+ continue
150
+ }
151
+
152
+ $root = Join-Path $candidate 'Autodesk'
153
+
154
+ if (-not (Test-Path -LiteralPath $root)) {
155
+ continue
156
+ }
157
+
158
+ if ($roots -notcontains $root) {
159
+ $null = $roots.Add($root)
160
+ }
161
+ }
162
+
163
+ return $roots.ToArray()
164
+ }
165
+
166
+ function Get-InstalledRevitVersions {
167
+ $found = New-Object System.Collections.ArrayList
168
+ $seen = New-Object System.Collections.ArrayList
169
+
170
+ foreach ($root in (Get-AutodeskRoots)) {
171
+ $directories = @(Get-ChildItem -LiteralPath $root -Directory -ErrorAction SilentlyContinue)
172
+
173
+ foreach ($directory in $directories) {
174
+ if ($directory.Name -notmatch '^Revit\s+(\d{4})$') {
175
+ continue
176
+ }
177
+
178
+ $year = $Matches[1]
179
+
180
+ if ($seen -contains $year) {
181
+ continue
182
+ }
183
+
184
+ $null = $seen.Add($year)
185
+
186
+ $null = $found.Add([pscustomobject]@{
187
+ Version = $year
188
+ RevitPath = $directory.FullName
189
+ HasExe = (Test-Path -LiteralPath (Join-Path $directory.FullName 'Revit.exe'))
190
+ })
191
+ }
192
+ }
193
+
194
+ $sorted = @($found.ToArray() | Sort-Object -Property @{ Expression = { [int] $_.Version } })
195
+ return $sorted
196
+ }
197
+
198
+ function Get-AddinsRoot {
199
+ $path = Join-Path $env:APPDATA 'Autodesk'
200
+ $path = Join-Path $path 'Revit'
201
+ $path = Join-Path $path 'Addins'
202
+ return $path
203
+ }
204
+
205
+ # Versions that already have an add-ins folder. Used by -Uninstall so the bridge can still be
206
+ # cleaned up after the matching Revit itself has been removed.
207
+ function Get-ConfiguredVersions {
208
+ $root = Get-AddinsRoot
209
+ $years = New-Object System.Collections.ArrayList
210
+
211
+ if (-not (Test-Path -LiteralPath $root)) {
212
+ return $years.ToArray()
213
+ }
214
+
215
+ $directories = @(Get-ChildItem -LiteralPath $root -Directory -ErrorAction SilentlyContinue)
216
+
217
+ foreach ($directory in $directories) {
218
+ if ($directory.Name -match '^\d{4}$') {
219
+ $null = $years.Add($directory.Name)
220
+ }
221
+ }
222
+
223
+ return $years.ToArray()
224
+ }
225
+
226
+ # ---------------------------------------------------------------------------------------------
227
+ # Build
228
+ # ---------------------------------------------------------------------------------------------
229
+
230
+ # The add-in the npm package ships: already compiled, so no SDK is needed to install it.
231
+ function Resolve-PrebuiltOutput {
232
+ $distDir = Join-Path $ScriptRoot $DistDirName
233
+
234
+ if (-not (Test-Path -LiteralPath (Join-Path $distDir $AssemblyFileName))) {
235
+ return $null
236
+ }
237
+
238
+ return $distDir
239
+ }
240
+
241
+ function Test-DotnetAvailable {
242
+ $command = Get-Command 'dotnet' -ErrorAction SilentlyContinue
243
+
244
+ if ($null -eq $command) {
245
+ return $false
246
+ }
247
+
248
+ return $true
249
+ }
250
+
251
+ function Invoke-Build {
252
+ $projectPath = Join-Path $ScriptRoot $ProjectFileName
253
+
254
+ if (-not (Test-Path -LiteralPath $projectPath)) {
255
+ Write-Err ('Project not found: ' + $projectPath)
256
+ return $false
257
+ }
258
+
259
+ # Reached only when there is no prebuilt add-in to fall back on, so name both ways out.
260
+ if (-not (Test-DotnetAvailable)) {
261
+ $script:BuildStatus = 'failed'
262
+ Write-Err ('The .NET SDK is not installed (no "dotnet" on PATH) and there is no prebuilt add-in at ' + (Join-Path $ScriptRoot $DistDirName) + '. Either install the .NET SDK from https://dotnet.microsoft.com/download and re-run, or reinstall the npm package @rui.branco/revit-mcp, which ships the compiled add-in and needs no SDK at all.')
263
+ return $false
264
+ }
265
+
266
+ Write-Line -Level 'BUILD' -Message 'status=started configuration=Release'
267
+
268
+ # dotnet writes diagnostics to stderr; with ErrorActionPreference=Stop a merged 2>&1 stream
269
+ # would throw NativeCommandError, so relax it just for the call.
270
+ $previous = $ErrorActionPreference
271
+ $ErrorActionPreference = 'Continue'
272
+ $output = & dotnet build $projectPath -c Release --nologo 2>&1
273
+ $code = $LASTEXITCODE
274
+ $ErrorActionPreference = $previous
275
+
276
+ foreach ($entry in $output) {
277
+ $text = [string] $entry
278
+
279
+ if ([string]::IsNullOrWhiteSpace($text)) {
280
+ continue
281
+ }
282
+
283
+ Write-Line -Level 'BUILD' -Message ('| ' + $text.Trim())
284
+ }
285
+
286
+ if ($code -ne 0) {
287
+ $script:BuildStatus = 'failed'
288
+ Write-Err ('dotnet build failed with exit code ' + $code + '.')
289
+ return $false
290
+ }
291
+
292
+ $script:BuildStatus = 'ok'
293
+ Write-Line -Level 'BUILD' -Message 'status=ok'
294
+ return $true
295
+ }
296
+
297
+ function Resolve-BuildOutput {
298
+ $binRoot = Join-Path $ScriptRoot 'bin'
299
+
300
+ if (-not (Test-Path -LiteralPath $binRoot)) {
301
+ return $null
302
+ }
303
+
304
+ $matchesFound = @(
305
+ Get-ChildItem -LiteralPath $binRoot -Recurse -File -Filter $AssemblyFileName -ErrorAction SilentlyContinue |
306
+ Where-Object { $_.DirectoryName -like '*\Release*' } |
307
+ Sort-Object -Property LastWriteTimeUtc -Descending
308
+ )
309
+
310
+ if ($matchesFound.Count -eq 0) {
311
+ return $null
312
+ }
313
+
314
+ return $matchesFound[0].DirectoryName
315
+ }
316
+
317
+ # ---------------------------------------------------------------------------------------------
318
+ # Install / uninstall for one version
319
+ # ---------------------------------------------------------------------------------------------
320
+ function New-AddinManifest {
321
+ param(
322
+ [string] $AssemblyPath
323
+ )
324
+
325
+ # Paths can legitimately contain & - escape before embedding in XML.
326
+ $safePath = [System.Security.SecurityElement]::Escape($AssemblyPath)
327
+
328
+ $lines = @(
329
+ '<?xml version="1.0" encoding="utf-8"?>',
330
+ '<RevitAddIns>',
331
+ ' <AddIn Type="Application">',
332
+ (' <Name>' + $AddInName + '</Name>'),
333
+ (' <Assembly>' + $safePath + '</Assembly>'),
334
+ (' <AddInId>' + $AddInId + '</AddInId>'),
335
+ (' <FullClassName>' + $FullClassName + '</FullClassName>'),
336
+ (' <VendorId>' + $VendorId + '</VendorId>'),
337
+ (' <VendorDescription>' + $VendorDescription + '</VendorDescription>'),
338
+ ' </AddIn>',
339
+ '</RevitAddIns>'
340
+ )
341
+
342
+ return ($lines -join [Environment]::NewLine)
343
+ }
344
+
345
+ function Install-ForVersion {
346
+ param(
347
+ [psobject] $Target,
348
+ [string] $OutputDir
349
+ )
350
+
351
+ $version = $Target.Version
352
+ $addinsDir = Join-Path (Get-AddinsRoot) $version
353
+ $installDir = Join-Path $addinsDir $AddInName
354
+ $manifest = Join-Path $addinsDir $ManifestFileName
355
+ $assembly = Join-Path $installDir $AssemblyFileName
356
+
357
+ # Idempotent: drop the previous copy wholesale rather than merging into it, so a renamed or
358
+ # removed file from an older build cannot linger.
359
+ if (Test-Path -LiteralPath $installDir) {
360
+ Remove-Item -LiteralPath $installDir -Recurse -Force
361
+ }
362
+
363
+ $null = New-Item -ItemType Directory -Path $installDir -Force
364
+
365
+ $copied = 0
366
+
367
+ foreach ($file in @(Get-ChildItem -LiteralPath $OutputDir -File)) {
368
+ if ($RevitOwnedFiles -contains $file.Name) {
369
+ Write-Warn ('Not copying ' + $file.Name + ' - Revit supplies it at runtime. Check that the Revit API PackageReference is still ExcludeAssets=runtime.')
370
+ continue
371
+ }
372
+
373
+ Copy-Item -LiteralPath $file.FullName -Destination $installDir -Force
374
+ $copied = $copied + 1
375
+ }
376
+
377
+ foreach ($directory in @(Get-ChildItem -LiteralPath $OutputDir -Directory)) {
378
+ Copy-Item -LiteralPath $directory.FullName -Destination $installDir -Recurse -Force
379
+ }
380
+
381
+ Set-Content -LiteralPath $manifest -Value (New-AddinManifest -AssemblyPath $assembly) -Encoding utf8
382
+
383
+ Write-Line -Level 'INSTALLED' -Message ('version=' + $version + ' files=' + $copied + ' dir="' + $installDir + '" manifest="' + $manifest + '"')
384
+
385
+ $null = $script:VersionResults.Add([ordered]@{
386
+ version = $version
387
+ revitPath = $Target.RevitPath
388
+ addinsDir = $addinsDir
389
+ installDir = $installDir
390
+ manifest = $manifest
391
+ assembly = $assembly
392
+ files = $copied
393
+ status = 'installed'
394
+ })
395
+ }
396
+
397
+ function Uninstall-ForVersion {
398
+ param(
399
+ [psobject] $Target
400
+ )
401
+
402
+ $version = $Target.Version
403
+ $addinsDir = Join-Path (Get-AddinsRoot) $version
404
+ $installDir = Join-Path $addinsDir $AddInName
405
+ $manifest = Join-Path $addinsDir $ManifestFileName
406
+
407
+ $removedDir = $false
408
+ $removedManifest = $false
409
+
410
+ if (Test-Path -LiteralPath $installDir) {
411
+ Remove-Item -LiteralPath $installDir -Recurse -Force
412
+ $removedDir = $true
413
+ }
414
+
415
+ if (Test-Path -LiteralPath $manifest) {
416
+ Remove-Item -LiteralPath $manifest -Force
417
+ $removedManifest = $true
418
+ }
419
+
420
+ $status = 'not-installed'
421
+
422
+ if ($removedDir -or $removedManifest) {
423
+ $status = 'removed'
424
+ }
425
+
426
+ Write-Line -Level 'REMOVED' -Message ('version=' + $version + ' status=' + $status + ' dir="' + $installDir + '" manifest="' + $manifest + '"')
427
+
428
+ $null = $script:VersionResults.Add([ordered]@{
429
+ version = $version
430
+ revitPath = $Target.RevitPath
431
+ addinsDir = $addinsDir
432
+ installDir = $installDir
433
+ manifest = $manifest
434
+ removedFolder = $removedDir
435
+ removedManifest = $removedManifest
436
+ status = $status
437
+ })
438
+ }
439
+
440
+ # ---------------------------------------------------------------------------------------------
441
+ # Main
442
+ # ---------------------------------------------------------------------------------------------
443
+ function Invoke-Main {
444
+ $action = 'install'
445
+
446
+ if ($Uninstall) {
447
+ $action = 'uninstall'
448
+ }
449
+
450
+ Write-Line -Level 'INFO' -Message ('action=' + $action + ' addin=' + $AddInName + ' minimumRevit=' + $MinimumRevitYear)
451
+
452
+ $detected = @(Get-InstalledRevitVersions)
453
+
454
+ foreach ($entry in $detected) {
455
+ Write-Line -Level 'DETECTED' -Message ('version=' + $entry.Version + ' hasExe=' + $entry.HasExe + ' path="' + $entry.RevitPath + '"')
456
+ }
457
+
458
+ $candidates = New-Object System.Collections.ArrayList
459
+
460
+ foreach ($entry in $detected) {
461
+ $null = $candidates.Add($entry)
462
+ }
463
+
464
+ # -Uninstall also cleans versions whose Revit is gone but whose add-ins folder remains.
465
+ if ($Uninstall) {
466
+ foreach ($year in (Get-ConfiguredVersions)) {
467
+ $known = @($candidates.ToArray() | Where-Object { $_.Version -eq $year })
468
+
469
+ if ($known.Count -eq 0) {
470
+ Write-Line -Level 'DETECTED' -Message ('version=' + $year + ' hasExe=False path="" source=addins-folder')
471
+
472
+ $null = $candidates.Add([pscustomobject]@{
473
+ Version = $year
474
+ RevitPath = $null
475
+ HasExe = $false
476
+ })
477
+ }
478
+ }
479
+ }
480
+
481
+ # --- honour -RevitVersion ------------------------------------------------------------------
482
+ if (-not [string]::IsNullOrWhiteSpace($RevitVersion)) {
483
+ $wanted = $RevitVersion.Trim()
484
+
485
+ if ($wanted -notmatch '^\d{4}$') {
486
+ Write-Err ('-RevitVersion must be a four-digit year such as 2026, but was "' + $RevitVersion + '".')
487
+ $script:ExitCode = 3
488
+ return
489
+ }
490
+
491
+ $selected = @($candidates.ToArray() | Where-Object { $_.Version -eq $wanted })
492
+
493
+ if ($selected.Count -eq 0) {
494
+ Write-Warn ('Revit ' + $wanted + ' was not found under Program Files. Continuing anyway because -RevitVersion was given explicitly; the add-ins folder does not require Revit to be installed in the default location.')
495
+
496
+ $selected = @([pscustomobject]@{
497
+ Version = $wanted
498
+ RevitPath = $null
499
+ HasExe = $false
500
+ })
501
+ }
502
+ }
503
+ else {
504
+ if ($candidates.Count -eq 0) {
505
+ if ($Uninstall) {
506
+ Write-Err 'Nothing to uninstall: no Revit installation was found under Program Files\Autodesk (looked for folders named "Revit <year>") and no add-ins folder exists under %APPDATA%\Autodesk\Revit\Addins. Pass -RevitVersion <year> to target a version explicitly.'
507
+ }
508
+ else {
509
+ Write-Err 'No Revit installation was found under Program Files\Autodesk (looked for folders named "Revit <year>"). Install Revit, or pass -RevitVersion <year> to target a version explicitly.'
510
+ }
511
+
512
+ $script:ExitCode = 2
513
+ return
514
+ }
515
+
516
+ $selected = @($candidates.ToArray())
517
+ }
518
+
519
+ # --- .NET Framework gate --------------------------------------------------------------------
520
+ # Revit 2024 and earlier run add-ins on .NET Framework 4.8; this project targets net8.0-windows
521
+ # and simply cannot be loaded by them. Installing anyway would produce a confusing load error
522
+ # inside Revit instead of a clear message here.
523
+ #
524
+ # The gate applies to installs only. -Uninstall is allowed to clean any version, because a
525
+ # leftover folder under a 2024 add-ins directory should still be removable.
526
+ $supported = New-Object System.Collections.ArrayList
527
+
528
+ foreach ($entry in $selected) {
529
+ if ((-not $Uninstall) -and ([int] $entry.Version -lt $MinimumRevitYear)) {
530
+ Write-Warn ('Revit ' + $entry.Version + ' runs add-ins on .NET Framework 4.8 and cannot load this build (it targets net8.0-windows). Revit ' + $MinimumRevitYear + ' or newer is required.')
531
+
532
+ $null = $script:VersionResults.Add([ordered]@{
533
+ version = $entry.Version
534
+ status = 'skipped-unsupported'
535
+ reason = 'Revit ' + $entry.Version + ' is .NET Framework based; this build requires Revit ' + $MinimumRevitYear + '+.'
536
+ })
537
+
538
+ continue
539
+ }
540
+
541
+ $null = $supported.Add($entry)
542
+ }
543
+
544
+ if ($supported.Count -eq 0) {
545
+ Write-Err ('No supported Revit version to work with. This build requires Revit ' + $MinimumRevitYear + ' or newer.')
546
+ $script:ExitCode = 3
547
+ return
548
+ }
549
+
550
+ # --- uninstall --------------------------------------------------------------------------------
551
+ if ($Uninstall) {
552
+ foreach ($entry in $supported) {
553
+ Uninstall-ForVersion -Target $entry
554
+ }
555
+
556
+ Write-Line -Level 'RESULT' -Message ('status=ok action=uninstall versions=' + (($supported.ToArray() | ForEach-Object { $_.Version }) -join ','))
557
+ Write-Line -Level 'DONE' -Message 'Restart Revit to load the bridge'
558
+ $script:ExitCode = 0
559
+ return
560
+ }
561
+
562
+ # --- pick the add-in to install ----------------------------------------------------------------
563
+ # Default: the prebuilt DLL in dist\, which is what the npm package ships - no .NET SDK, no
564
+ # build step. -Build forces a fresh compile instead, for someone working on the C# side. Only a
565
+ # source checkout with no dist\ ever falls through to building on its own.
566
+ $outputDir = $null
567
+
568
+ if ($Build) {
569
+ if (-not (Invoke-Build)) {
570
+ $script:ExitCode = 4
571
+ return
572
+ }
573
+
574
+ $outputDir = Resolve-BuildOutput
575
+ }
576
+ else {
577
+ $outputDir = Resolve-PrebuiltOutput
578
+
579
+ if (-not [string]::IsNullOrWhiteSpace($outputDir)) {
580
+ $script:BuildStatus = 'prebuilt'
581
+ Write-Line -Level 'BUILD' -Message 'status=skipped reason=prebuilt-dist'
582
+ }
583
+ elseif ($SkipBuild) {
584
+ Write-Line -Level 'BUILD' -Message 'status=skipped reason=-SkipBuild'
585
+ $outputDir = Resolve-BuildOutput
586
+ }
587
+ else {
588
+ if (-not (Invoke-Build)) {
589
+ $script:ExitCode = 4
590
+ return
591
+ }
592
+
593
+ $outputDir = Resolve-BuildOutput
594
+ }
595
+ }
596
+
597
+ if ([string]::IsNullOrWhiteSpace($outputDir)) {
598
+ Write-Err ('Nothing to install: no prebuilt ' + $AssemblyFileName + ' in ' + (Join-Path $ScriptRoot $DistDirName) + ' and no Release build output under ' + (Join-Path $ScriptRoot 'bin') + '. Re-run with -Build to compile one (needs the .NET SDK), or reinstall the npm package @rui.branco/revit-mcp, which ships the add-in already compiled.')
599
+ $script:ExitCode = 5
600
+ return
601
+ }
602
+
603
+ $script:OutputDir = $outputDir
604
+ Write-Line -Level 'OUTPUT' -Message ('dir="' + $outputDir + '"')
605
+
606
+ # --- install ----------------------------------------------------------------------------------
607
+ foreach ($entry in $supported) {
608
+ Install-ForVersion -Target $entry -OutputDir $outputDir
609
+ }
610
+
611
+ Write-Line -Level 'RESULT' -Message ('status=ok action=install versions=' + (($supported.ToArray() | ForEach-Object { $_.Version }) -join ','))
612
+ Write-Line -Level 'DONE' -Message 'Restart Revit to load the bridge'
613
+ $script:ExitCode = 0
614
+ }
615
+
616
+ try {
617
+ Invoke-Main
618
+ }
619
+ catch {
620
+ Write-Err ('Unhandled failure: ' + $_.Exception.Message)
621
+
622
+ if ($null -ne $_.ScriptStackTrace) {
623
+ $null = $script:Log.Add('[revit-bridge] TRACE ' + $_.ScriptStackTrace)
624
+ }
625
+
626
+ if ($script:ExitCode -eq 0) {
627
+ $script:ExitCode = 1
628
+ }
629
+ }
630
+
631
+ if ($Json) {
632
+ $action = 'install'
633
+
634
+ if ($Uninstall) {
635
+ $action = 'uninstall'
636
+ }
637
+
638
+ $ok = $false
639
+
640
+ if ($script:ExitCode -eq 0) {
641
+ $ok = $true
642
+ }
643
+
644
+ $message = 'Restart Revit to load the bridge'
645
+
646
+ if (-not $ok) {
647
+ $message = 'The Revit MCP Bridge installer failed; see errors.'
648
+ }
649
+
650
+ $result = [ordered]@{
651
+ ok = $ok
652
+ action = $action
653
+ exitCode = $script:ExitCode
654
+ message = $message
655
+ addInName = $AddInName
656
+ addInId = $AddInId
657
+ fullClassName = $FullClassName
658
+ vendorId = $VendorId
659
+ buildStatus = $script:BuildStatus
660
+ outputDir = $script:OutputDir
661
+ versions = @($script:VersionResults.ToArray())
662
+ warnings = @($script:Warnings.ToArray())
663
+ errors = @($script:Errors.ToArray())
664
+ log = @($script:Log.ToArray())
665
+ }
666
+
667
+ # The @(...) wrappers above matter: they keep one-element and empty collections rendering as
668
+ # real JSON arrays under Windows PowerShell 5.1, so the caller never has to special-case them.
669
+ # Not named $json: PowerShell variable names are case-insensitive, so that would clobber the
670
+ # [switch] $Json parameter with a string and blow up parameter binding.
671
+ $jsonText = ConvertTo-Json -InputObject $result -Depth 12
672
+
673
+ [Console]::Out.WriteLine($jsonText)
674
+ }
675
+
676
+ exit $script:ExitCode