Skip to content

Add output truncation for large command results to preserve AI context window#34

Open
doraemonkeys wants to merge 4 commits intoyotsuda:mainfrom
doraemonkeys:main
Open

Add output truncation for large command results to preserve AI context window#34
doraemonkeys wants to merge 4 commits intoyotsuda:mainfrom
doraemonkeys:main

Conversation

@doraemonkeys
Copy link

Summary

Adds OutputTruncationHelper to automatically truncate large command outputs, preserving AI context window budget.
The design is inspired by Claude Code's Bash tool truncation mechanism — when Bash output exceeds the size limit,
Claude Code saves the full content to a temp file and returns a head preview with the file path, allowing the AI to
retrieve the full output via the Read tool when needed. We adopt the same pattern for PowerShell.MCP's
invoke_expression responses.

Changes

  • Introduced OutputTruncationHelper that truncates outputs exceeding 5,000 characters
  • When truncation occurs, the full output is saved to a temp file and a head+tail preview (~1,000 chars each) is
    returned with the file path for retrieval via Get-Content or the Read tool
  • Head and tail boundaries are aligned to the nearest newline for cleaner previews
  • Includes opportunistic cleanup of temp files older than 120 minutes
  • Added compile-time validation ensuring the truncation threshold exceeds the combined preview sizes to prevent
    overlapping head/tail content
  • Applied truncation to all response paths in PowerShellTools (12 call sites)
  • Added InternalsVisibleTo for the test project
  • Comprehensive unit tests covering threshold boundaries, newline-aligned cutting, file persistence, graceful
    degradation on disk failure, and temp file cleanup

Design rationale

Claude Code's Bash tool handles large outputs by saving the full content to a file and returning a truncated preview
with the file path. Without similar handling, PowerShell.MCP
would return unbounded output directly into the AI's context window, wasting tokens and potentially hitting context
limits. This PR brings the same "preview + file fallback" pattern to all PowerShell tool responses.

- Introduced OutputTruncationHelper class to truncate large outputs, saving full content to a temporary file for retrieval.
- Updated PowerShellTools to utilize OutputTruncationHelper for response handling, ensuring outputs are truncated when exceeding the defined threshold.
- Added unit tests for OutputTruncationHelper to validate functionality, including edge cases for output size and file saving.
@yotsuda
Copy link
Owner

yotsuda commented Feb 15, 2026

Thank you for this well-designed PR! The code quality is high — clean implementation, proper error handling with graceful degradation, newline-aligned boundaries, and comprehensive tests. All 216 tests pass (including the 17 new ones).

However, I have an architectural concern about where the truncation happens.

Truncation should happen in the DLL module, not the Proxy

Currently, the truncation logic lives in PowerShell.MCP.Proxy. This means the full output travels through the named pipe from the DLL module to the Proxy, and only then gets truncated and saved to a temp file on the Proxy side.

graph TD
    A["PowerShell (command execution)"] -->|"full output"| B["PowerShell.MCP dll module"]
    B -->|"⚠️ full output still flows through named pipe"| C["MCP Server (proxy exe)<br/>★ truncation + file save happens here"]
    C -->|"preview only"| D["AI client"]

    style C fill:#fee,stroke:#c00,color:#900
Loading

The truncation should instead happen in the DLL module, right after the pipeline execution completes. This way:

  1. The named pipe transfer is reduced — only the truncated preview crosses the pipe, not the full output
  2. The file save happens in the same process that executed the command — no round-trip of large data
  3. Cached results should also be truncated — when a large result is cached (e.g., for wait_for_completion), the full output should be saved to a file, and the cache should store the truncated preview with the file path. This avoids holding large strings in memory and keeps the cache lightweight.

Truncation threshold is too low

The current TruncationThreshold is hardcoded at 5,000 characters. For reference, Claude Code's Bash tool truncates at 30,000 characters. A 5,000-character threshold will trigger truncation on many common PowerShell commands (Get-Process, Get-ChildItem -Recurse, Get-Help, etc.), causing unnecessary round-trips for the AI to retrieve the full output. I'd recommend aligning with the 30,000-character threshold.


The OutputTruncationHelper class itself is well-designed and can be reused as-is — it just needs to be moved to the DLL module side and applied at the right point in the pipeline execution flow.

Looking forward to a revised version!

… DLL module

Truncation now happens in PowerShellCommunication.NotifyResultReady() before
caching, so only the truncated preview crosses the named pipe — reducing pipe
transfer size and memory overhead. Full output is saved to a temp file on the
DLL side.

- Add PowerShell.MCP.Shared project with OutputTruncationHelper (threshold 15K)
- Remove OutputTruncationHelper from Proxy/Helpers (no longer needed there)
- Add NotifySilentResultReady() for small known-output internal paths
- Update project references and solution file
- Move tests to Tests/Unit/Shared/
@doraemonkeys
Copy link
Author

Thanks for the review! All three concerns have been addressed:

  1. Truncation moved to the DLL module — Done. OutputTruncationHelper now lives in a new PowerShell.MCP.Shared project referenced by both the DLL and Proxy. Truncation happens in PowerShellCommunication.NotifyResultReady() before caching, so only the truncated preview crosses the named pipe.

  2. Cached results are truncated — Done. Full output is saved to a temp file, and the cache stores only the truncated preview with the file path. Old files are cleaned up after 2 hours.

  3. Truncation threshold — I've set it to 15,000 characters rather than 30,000.

The Claude Code Bash tool's 30K threshold (~7.5K tokens) is ~3.75% of a 200K context window. A single call is fine, but users often execute multiple commands in succession. If each returns close to the threshold, context consumption adds up quickly. I believe 15K already strikes a good balance between usability and context efficiency.

@yotsuda
Copy link
Owner

yotsuda commented Feb 15, 2026

@doraemonkeys Thank you for this contribution! The feature itself is very useful — truncating large outputs to preserve the AI context window is exactly the right approach, and the "preview + file fallback" pattern inspired by Claude Code's Bash tool is a great design choice.

I have some feedback before we can merge this.

Architecture: Remove PowerShell.MCP.Shared, keep changes in the DLL only

The main feedback is about where truncation is applied. Currently the PR:

  1. Creates a new PowerShell.MCP.Shared project
  2. Applies truncation in NotifyResultReady() on the DLL side
  3. Also applies truncation in PowerShellTools.cs on the Proxy side (12 call sites)

This can be simplified significantly. Looking at the output flow:

flowchart LR
    A["PowerShell<br/>execution result"] --> B["NotifyResultReady()<br/>☑ truncate here"]
    B --> C["ExecutionState<br/>cache"]
    C --> D[Named Pipe]
    D --> E["PowerShellTools<br/>✗ no changes needed"]
    style B fill:#d4edda,stroke:#28a745,color:#155724
    style E fill:#f8d7da,stroke:#dc3545,color:#721c24
Loading

If truncation happens in NotifyResultReady(), the Proxy already receives truncated output. No changes to the Proxy are needed at all, which means:

  • OutputTruncationHelper should live directly in PowerShell.MCP (not in a Shared project)
  • PowerShell.MCP.Shared project is unnecessary — remove it
  • All 12 TruncateIfNeeded() calls in PowerShellTools.cs can be removed
  • No changes needed to PowerShell.MCP.Proxy.csproj

This reduces the diff substantially and keeps the Proxy untouched.

Other items

InternalsVisibleTo on Proxy project

With OutputTruncationHelper moved out of the Proxy, the InternalsVisibleTo addition to PowerShell.MCP.Proxy.csproj is no longer needed.

Truncation message should guide the AI to use Show-TextFiles

When output is truncated and saved to a file, the current message suggests:

sb.AppendLine($"Use invoke_expression('Get-Content \"{filePath}\"') or Read tool to access the full output.");

Two problems here:

  1. Get-Content will be truncated again — reading the full file via invoke_expression will produce the same large output, which will be truncated by the very same mechanism. This creates an unrecoverable loop.
  2. Read tool is a Claude Code built-in, not a PowerShell.MCP tool. Other MCP clients may not have it.

The message should instead suggest Show-TextFiles with -Contains or -Pattern to search the saved file:

sb.AppendLine($"Use invoke_expression('Show-TextFiles \"{filePath}\" -Contains \"search term\"') or -Pattern \"regex\" to search the output.");

This is consistent with PowerShell.MCP's own tool description, which recommends Show-TextFiles for reading text files.

Newline alignment test assertions

The HeadAlignsToNewline and TailAlignsToNewline tests use Assert.Contains("\n", previewSection), but previewSection includes metadata lines like "Output too large..." and "--- Preview ---" which always contain newlines. This means the assertion always passes regardless of whether the newline alignment logic works correctly. Consider asserting the actual boundary position instead.


Overall this is a well-implemented feature with solid test coverage. The main ask is the architectural simplification — once OutputTruncationHelper lives solely in the DLL project and the Proxy changes are removed, this will be clean and ready to merge.

Truncation in NotifyResultReady() means the Proxy already receives
truncated output, so the 12 TruncateIfNeeded calls on the Proxy side
and the Shared project are unnecessary.

- Move OutputTruncationHelper into PowerShell.MCP (DLL project)
- Remove all TruncateIfNeeded calls from PowerShellTools.cs
- Remove PowerShell.MCP.Shared project and all references
- Fix truncation message: Get-Content → Show-TextFiles (avoids
  re-truncation loop)
- Strengthen newline alignment test assertions to verify actual
  boundary positions instead of vacuously passing on metadata newlines
@doraemonkeys
Copy link
Author

@doraemonkeys Thank you for this contribution! The feature itself is very useful — truncating large outputs to preserve the AI context window is exactly the right approach, and the "preview + file fallback" pattern inspired by Claude Code's Bash tool is a great design choice.

I have some feedback before we can merge this.

Architecture: Remove PowerShell.MCP.Shared, keep changes in the DLL only

The main feedback is about where truncation is applied. Currently the PR:

  1. Creates a new PowerShell.MCP.Shared project
  2. Applies truncation in NotifyResultReady() on the DLL side
  3. Also applies truncation in PowerShellTools.cs on the Proxy side (12 call sites)

This can be simplified significantly. Looking at the output flow:

flowchart LR
    A["PowerShell<br/>execution result"] --> B["NotifyResultReady()<br/>☑ truncate here"]
    B --> C["ExecutionState<br/>cache"]
    C --> D[Named Pipe]
    D --> E["PowerShellTools<br/>✗ no changes needed"]
    style B fill:#d4edda,stroke:#28a745,color:#155724
    style E fill:#f8d7da,stroke:#dc3545,color:#721c24
Loading

If truncation happens in NotifyResultReady(), the Proxy already receives truncated output. No changes to the Proxy are needed at all, which means:

  • OutputTruncationHelper should live directly in PowerShell.MCP (not in a Shared project)
  • PowerShell.MCP.Shared project is unnecessary — remove it
  • All 12 TruncateIfNeeded() calls in PowerShellTools.cs can be removed
  • No changes needed to PowerShell.MCP.Proxy.csproj

This reduces the diff substantially and keeps the Proxy untouched.

Other items

InternalsVisibleTo on Proxy project

With OutputTruncationHelper moved out of the Proxy, the InternalsVisibleTo addition to PowerShell.MCP.Proxy.csproj is no longer needed.

Truncation message should guide the AI to use Show-TextFiles

When output is truncated and saved to a file, the current message suggests:

sb.AppendLine($"Use invoke_expression('Get-Content \"{filePath}\"') or Read tool to access the full output.");

Two problems here:

  1. Get-Content will be truncated again — reading the full file via invoke_expression will produce the same large output, which will be truncated by the very same mechanism. This creates an unrecoverable loop.
  2. Read tool is a Claude Code built-in, not a PowerShell.MCP tool. Other MCP clients may not have it.

The message should instead suggest Show-TextFiles with -Contains or -Pattern to search the saved file:

sb.AppendLine($"Use invoke_expression('Show-TextFiles \"{filePath}\" -Contains \"search term\"') or -Pattern \"regex\" to search the output.");

This is consistent with PowerShell.MCP's own tool description, which recommends Show-TextFiles for reading text files.

Newline alignment test assertions

The HeadAlignsToNewline and TailAlignsToNewline tests use Assert.Contains("\n", previewSection), but previewSection includes metadata lines like "Output too large..." and "--- Preview ---" which always contain newlines. This means the assertion always passes regardless of whether the newline alignment logic works correctly. Consider asserting the actual boundary position instead.


Overall this is a well-implemented feature with solid test coverage. The main ask is the architectural simplification — once OutputTruncationHelper lives solely in the DLL project and the Proxy changes are removed, this will be clean and ready to merge.

Removed PowerShell.MCP.Shared — OutputTruncationHelper now lives directly in the DLL project

Removed all 12 TruncateIfNeeded calls from Proxy — since NotifyResultReady() already truncates before caching, the Proxy receives pre-truncated output

Removed InternalsVisibleTo from Proxy .csproj — no longer needed

Fixed truncation message — replaced Get-Content / Read tool with Show-TextFiles -Contains to avoid the re-truncation loop

Strengthened newline alignment tests — assertions now verify actual boundary positions instead of passing vacuously on metadata newlines

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants