Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions .github/instructions/source-cmdlets--csharp.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,13 +127,15 @@ Close `ProcessRecord`/`EndProcessing` with this exact catch ladder (copy from a
and the per-type `ErrorCategory` matter; each ends in `ThrowTerminatingError`:
```csharp
catch (TrisoftAutomationException e) { ThrowTerminatingError(new ErrorRecord(e, base.GetType().Name, ErrorCategory.InvalidOperation, null)); }
catch (OpenApiISH30.OpenApiISH30Exception<OpenApiISH30.InfoShareProblemDetails> e) {if (e.Result != null) { WriteWarning($"Status[{e.Result.Status}] Title[{e.Result.Title}] EventName[{e.Result.EventName}] Detail[{e.Result.Detail}]"); foreach (var error in e.Result.Errors) { WriteWarning($"ErrorEventName[{error.EventName}] ErrorDetail[{error.Detail}]"); } } ThrowTerminatingError(new ErrorRecord(e, base.GetType().Name, ErrorCategory.InvalidOperation, null)); }
catch (AggregateException e) { var f = e.Flatten(); WriteWarning(f.ToString()); ThrowTerminatingError(new ErrorRecord(f, base.GetType().Name, ErrorCategory.NotSpecified, null)); }
catch (TimeoutException e) { WriteVerbose(...); ThrowTerminatingError(new ErrorRecord(e, base.GetType().Name, ErrorCategory.OperationTimeout, null)); }
catch (CommunicationException e) { WriteVerbose(...); ThrowTerminatingError(new ErrorRecord(e, base.GetType().Name, ErrorCategory.OperationStopped, null)); }
catch (Exception e) { ThrowTerminatingError(new ErrorRecord(e, base.GetType().Name, ErrorCategory.NotSpecified, null)); }
catch (TimeoutException e) { WriteVerbose("TimeoutException Message[" + e.Message + "] StackTrace[" + e.StackTrace + "]"); ThrowTerminatingError(new ErrorRecord(e, base.GetType().Name, ErrorCategory.OperationTimeout, null)); }
catch (CommunicationException e) { WriteVerbose("CommunicationException Message[" + e.Message + "] StackTrace[" + e.StackTrace + "]"); ThrowTerminatingError(new ErrorRecord(e, base.GetType().Name, ErrorCategory.OperationStopped, null)); }
catch (Exception e) { if (e.InnerException != null) { WriteWarning(e.InnerException.ToString()); } ThrowTerminatingError(new ErrorRecord(e, base.GetType().Name, ErrorCategory.NotSpecified, null)); }
```
Don't reorder, collapse, or silently swallow these. If you believe the handling can genuinely be
improved, **challenge it explicitly with the implementer** before changing it.
The `OpenApiISH30Exception` catch is only required when the cmdlet makes OpenAPI calls; SOAP-only
cmdlets may omit it. Don't reorder, collapse, or silently swallow these. If you believe the
handling can genuinely be improved, **challenge it explicitly with the implementer** before changing it.

## 8. Diagnostic logging density
A `-Debug` or `-Verbose` transcript must contain enough context to reconstruct what happened and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,17 @@ Authoring detail for each topic lives in the companion instruction files injecte
## 6. Exception handling

- [ ] Catch ladder is in this exact order:
`TrisoftAutomationException` → `AggregateException` → `TimeoutException` →
`TrisoftAutomationException` → *(OpenAPI cmdlets only)* `OpenApiISH30Exception<InfoShareProblemDetails>` → `AggregateException` → `TimeoutException` →
`CommunicationException` → `Exception`.
- [ ] `OpenApiISH30Exception<InfoShareProblemDetails>` catch is present whenever the cmdlet makes
OpenAPI calls; SOAP-only cmdlets may omit it. When present it calls `WriteWarning` for each
structured error field (`Status`, `Title`, `EventName`, `Detail`, per-error `ErrorEventName`/`ErrorDetail`) before `ThrowTerminatingError`.
- [ ] Every catch calls `ThrowTerminatingError(new ErrorRecord(e, base.GetType().Name,
ErrorCategory.XXX, null))` with the correct `ErrorCategory` — no silent swallows, no plain
`throw`.
- [ ] `catch (Exception)` calls `WriteWarning(e.InnerException.ToString())` when `InnerException`
is non-null, before `ThrowTerminatingError` — surfaces the root cause of wrapped exceptions such
as `HttpRequestException`.
- [ ] The order is not reordered, collapsed, or partially removed without explicit sign-off from
the implementer.

Expand Down
4 changes: 3 additions & 1 deletion Doc/ReleaseNotes-ISHRemote-8.3.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ High level release notes are on [Github](https://github.com/rws/ISHRemote/releas

This release inherits the v0.1 to v0.14 up to v8.2 development branch and features. All cmdlets and business logic are fully compatible even around authentication. In short, we expect it all to work still :)

The one that is
The one that respects the details of Model Context Protocol (MCP) enabling usage in other toolings like Claude Code or OpenCode/OpenChamber. All `OpenApiWithOpenIdConnect` is now using in-flight http compression which benefits `Get-IshPublicationOutputData` offering faster downloads on 15.1.0+ environments.


### Remember
Expand All @@ -25,6 +25,8 @@ The below text describes the delta compared to fielded release ISHRemote v8.2.

## Implementation Details

* Enabled HTTP response compression (`gzip`, `deflate`, `brotli`) on the `HttpClient` for non-SOAP usage across protocols — `WcfSoapWithWsTrust`, `WcfSoapWithOpenIdConnect`, and `OpenApiWithOpenIdConnect`. Both `AutomaticDecompression` on the handler and the matching `Accept-Encoding` request headers are set, so the server can compress response bodies. On .NET 4.8 `brotli` is not available; `gzip` and `deflate` are used instead. See #232. Thanks @ddemeyer
* Augmented `Get-IshPublicationOutputData` with an `OpenApiWithOpenIdConnect` protocol implementation on Tridion Docs 15.1 and higher. Instead of a SOAP byte-level chunk loop (many sequential HTTP calls), a single streaming REST `GET /v3/Publications/ByLanguageCardId/{languageCardId}/Content` request is used, making large publication output downloads significantly faster especially over high-latency connections. Sessions using `OpenApiWithOpenIdConnect` on servers older than 15.1 transparently fall back to the existing SOAP chunk loop. Downloads are in essence network restricted, still streaming shows improvements between 20% and 40% compared to the SOAP variation on the same environment. See #245. Thanks @ddemeyer
* Fixed `Start-IshRemoteMcpServer` failing to connect on Windows with newer MCP clients (e.g. OpenCode 1.18.11, protocol `2025-11-25`) with errors `MCP error -32001: Request timed out` and `Failed to get tools`. Three root causes: (1) `initialize` requests with `"id":0` were silently dropped because PowerShell treats `0` as falsy; (2) `[Console]::InputEncoding` defaults to OEM code page (`ibm437`) when `pwsh.exe` is spawned with redirected stdio on Windows, causing `ReadLine()` to block forever on UTF-8 JSON — fixed by explicitly setting UTF-8 encoding and replacing `Console.Out` with an auto-flushing `StreamWriter` via `[Console]::SetOut()`; (3) `Register-IshRemoteMcpTool` emitted an invalid `type: "object"` field in `ToolAnnotations` and used string `"true"`/`"false"` instead of boolean `$true`/`$false` for hint values, causing strict MCP schema validation to reject the tools list. Server name updated from `"PowerShell MCP Server (Template)"` to `"ISHRemote MCP Server"` and version bumped to `0.3.0`. Also fixed the server looping forever on stdin EOF (orphaned `pwsh` processes) by breaking the while loop when `ReadLine()` returns `$null`. See #243 and #261. Thanks @ddemeyer


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,15 @@ namespace Trisoft.ISHRemote.Cmdlets.PublicationOutput
/// </summary>
/// <example>
/// <code>
/// $ishSession = New-IshSession -WsBaseUrl "https://example.com/InfoShareWS/" -IshUserName "username" -IshUserPassword "userpassword"
/// $ishSession = New-IshSession -WsBaseUrl "https://example.com/InfoShareWS/"
/// Get-IshPublicationOutput -LogicalId GUID-03081B9A-11E4-4862-845B-27339E0C400D |
/// Get-IshPublicationOutputData -FolderPath C:\TEMP\20260818\
/// </code>
/// <para>Retrieves all PublicationOutputs of the given logical id and downloads them.</para>
/// </example>
/// <example>
/// <code>
/// $ishSession = New-IshSession -WsBaseUrl "https://example.com/InfoShareWS/"
/// $requestedMetadataRetrieve = Set-IshRequestedMetadataField -IshSession $ishSession -Name 'FISHOUTPUTFORMATREF' -Level "Lng" |
/// Set-IshRequestedMetadataField -IshSession $ishSession -Name 'FISHPUBLNGCOMBINATION' -Level "Lng" |
/// Set-IshRequestedMetadataField -IshSession $ishSession -Name 'FISHPUBSTATUS' -Level "Lng" |
Expand Down Expand Up @@ -109,9 +117,10 @@ protected override void ProcessRecord()
{
// Get language ref
long lngRef = Convert.ToInt64(ishObject.ObjectRef[Enumerations.ReferenceType.Lng]);
string xmlIshDataObject = IshSession.PublicationOutput25.GetDataObjectInfoByIshLngRef(lngRef);

// Put the xml in a dataobject
// File extension is always sourced from GetDataObjectInfoByIshLngRef to guarantee
// an identical output filename regardless of the download protocol used.
string xmlIshDataObject = IshSession.PublicationOutput25.GetDataObjectInfoByIshLngRef(lngRef);
XmlDocument xmlIshDataObjectDocument = new XmlDocument();
xmlIshDataObjectDocument.LoadXml(xmlIshDataObject);
XmlElement ishDataObjectElement =
Expand All @@ -120,28 +129,51 @@ protected override void ProcessRecord()
string tempFilePath = FileNameHelper.GetDefaultPublicationOutputFileName(tempLocation, ishObject,
ishDataObject.FileExtension);

WriteDebug($"Writing lngRef[{lngRef}] to [{tempFilePath}] {++current}/{ishObjects.Length}");

//Create the file.
using (FileStream fs = File.Create(tempFilePath))
switch (IshSession.Protocol)
{
for (int offset = 0; offset < ishDataObject.Size; offset += IshSession.ChunkSize)
{
int size = IshSession.ChunkSize;
long offsetCount = offset;
byte[] byteArray = new byte[IshSession.ChunkSize];
var response =
IshSession.PublicationOutput25.GetNextDataObjectChunkByIshLngRef(
new PublicationOutput25ServiceReference.GetNextDataObjectChunkByIshLngRefRequest(
lngRef,
ishDataObject.Ed,
offsetCount,
size));
offsetCount = response.offSet;
size = response.size;
byteArray = response.bytes;
fs.Write(byteArray, 0, size);
}
case Enumerations.Protocol.OpenApiWithOpenIdConnect:
if (IshSession.ServerIshVersion.MajorVersion > 15 ||
(IshSession.ServerIshVersion.MajorVersion == 15 && IshSession.ServerIshVersion.MinorVersion >= 1))
{
// Single streaming HTTP GET via OpenAPI — replaces the entire SOAP chunk loop.
// IshSession.ChunkSize is reused as the CopyTo buffer size to keep memory usage
// bounded while avoiding unnecessary syscall overhead.
WriteDebug($"Writing lngRef[{lngRef}] via OpenAPI stream to [{tempFilePath}] {++current}/{ishObjects.Length}");
using (var fileResponse = IshSession.OpenApiISH30Client
.GetPublicationContentByLanguageCardIdAsync(lngRef)
.GetAwaiter().GetResult())
using (FileStream fs = File.Create(tempFilePath))
{
fileResponse.Stream.CopyTo(fs, IshSession.ChunkSize);
}
break;
}
// Server < 15.1 does not expose the REST endpoint; fall back to SOAP chunk loop.
goto case Enumerations.Protocol.WcfSoapWithOpenIdConnect;
case Enumerations.Protocol.WcfSoapWithWsTrust:
case Enumerations.Protocol.WcfSoapWithOpenIdConnect:
WriteDebug($"Writing lngRef[{lngRef}] to [{tempFilePath}] {++current}/{ishObjects.Length}");
using (FileStream fs = File.Create(tempFilePath))
{
for (int offset = 0; offset < ishDataObject.Size; offset += IshSession.ChunkSize)
{
int size = IshSession.ChunkSize;
long offsetCount = offset;
byte[] byteArray = new byte[IshSession.ChunkSize];
var response =
IshSession.PublicationOutput25.GetNextDataObjectChunkByIshLngRef(
new PublicationOutput25ServiceReference.GetNextDataObjectChunkByIshLngRefRequest(
lngRef,
ishDataObject.Ed,
offsetCount,
size));
offsetCount = response.offSet;
size = response.size;
byteArray = response.bytes;
fs.Write(byteArray, 0, size);
}
}
break;
}

// Append file info list
Expand All @@ -155,6 +187,18 @@ protected override void ProcessRecord()
{
ThrowTerminatingError(new ErrorRecord(trisoftAutomationException, base.GetType().Name, ErrorCategory.InvalidOperation, null));
}
catch (OpenApiISH30.OpenApiISH30Exception<OpenApiISH30.InfoShareProblemDetails> openApiISH30Exception)
{
if (openApiISH30Exception.Result != null)
{
WriteWarning($"Status[{openApiISH30Exception.Result.Status}] Title[{openApiISH30Exception.Result.Title}] EventName[{openApiISH30Exception.Result.EventName}] Detail[{openApiISH30Exception.Result.Detail}]");
foreach (var error in openApiISH30Exception.Result.Errors)
{
WriteWarning($"ErrorEventName[{error.EventName}] ErrorDetail[{error.Detail}]");
}
}
ThrowTerminatingError(new ErrorRecord(openApiISH30Exception, base.GetType().Name, ErrorCategory.InvalidOperation, null));
}
catch (AggregateException aggregateException)
{
var flattenedAggregateException = aggregateException.Flatten();
Expand All @@ -173,6 +217,7 @@ protected override void ProcessRecord()
}
catch (Exception exception)
{
if (exception.InnerException != null) { WriteWarning(exception.InnerException.ToString()); }
ThrowTerminatingError(new ErrorRecord(exception, base.GetType().Name, ErrorCategory.NotSpecified, null));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1726,7 +1726,20 @@ private XDocument LoadConnectionConfiguration()
handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
}
handler.SslProtocols = (System.Security.Authentication.SslProtocols)(SecurityProtocolType.Tls | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12 | SecurityProtocolType.Tls13);
#if NET48
handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
#else
handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Brotli;
#endif
var httpClient = new HttpClient(handler);
#if NET48
httpClient.DefaultRequestHeaders.AcceptEncoding.ParseAdd("gzip");
httpClient.DefaultRequestHeaders.AcceptEncoding.ParseAdd("deflate");
#else
httpClient.DefaultRequestHeaders.AcceptEncoding.ParseAdd("gzip");
httpClient.DefaultRequestHeaders.AcceptEncoding.ParseAdd("deflate");
httpClient.DefaultRequestHeaders.AcceptEncoding.ParseAdd("br");
#endif
httpClient.Timeout = _connectionParameters.Timeout;
var connectionConfigurationUri = new Uri(InfoShareWSBaseUri, "connectionconfiguration.xml");
_logger.WriteDebug($"LoadConnectionConfiguration uri[{connectionConfigurationUri}] timeout[{httpClient.Timeout}]");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1368,7 +1368,20 @@ private XDocument LoadConnectionConfiguration()
handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
}
handler.SslProtocols = (System.Security.Authentication.SslProtocols)(SecurityProtocolType.Tls | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12 | SecurityProtocolType.Tls13);
#if NET48
handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
#else
handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Brotli;
#endif
var httpClient = new HttpClient(handler);
#if NET48
httpClient.DefaultRequestHeaders.AcceptEncoding.ParseAdd("gzip");
httpClient.DefaultRequestHeaders.AcceptEncoding.ParseAdd("deflate");
#else
httpClient.DefaultRequestHeaders.AcceptEncoding.ParseAdd("gzip");
httpClient.DefaultRequestHeaders.AcceptEncoding.ParseAdd("deflate");
httpClient.DefaultRequestHeaders.AcceptEncoding.ParseAdd("br");
#endif
httpClient.Timeout = _connectionParameters.Timeout;
var connectionConfigurationUri = new Uri(InfoShareWSBaseUri, "connectionconfiguration.xml");
_logger.WriteDebug($"LoadConnectionConfiguration uri[{connectionConfigurationUri}] timeout[{httpClient.Timeout}]");
Expand Down
Loading
Loading