Skip to content

Commit 70bf200

Browse files
Proxy stdio traffic (#1485)
* Basics * Logging * MCP fix * Plugins * LogRequest * DevToolPlugin support * Logging to Dev Tools Plugin * Proper name * Fix display name * Misc fixes * Mock stdio * Fixes * Update MockStdioResponsePlugin schema to version 2.1.0 and add configuration schema * Adds support for specifying config for stdio * Update DevProxy.Abstractions/Models/MockStdioResponse.cs Co-authored-by: Copilot <[email protected]> * Review fixes --------- Co-authored-by: Copilot <[email protected]>
1 parent 96bc8cc commit 70bf200

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+2945
-162
lines changed

DevProxy.Abstractions/Extensions/ILoggerExtensions.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,34 @@ public static void LogRequest(this ILogger logger, string message, MessageType m
1717
logger.Log(new RequestLog(message, messageType, method, url));
1818
}
1919

20+
public static void LogRequest(this ILogger logger, string message, MessageType messageType, StdioLoggingContext context)
21+
{
22+
logger.Log(new StdioLogEntry(message, messageType, context));
23+
}
24+
2025
public static void Log(this ILogger logger, RequestLog message)
2126
{
2227
ArgumentNullException.ThrowIfNull(logger);
2328

2429
logger.Log(LogLevel.Information, 0, message, exception: null, (m, _) => JsonSerializer.Serialize(m));
2530
}
31+
32+
public static void Log(this ILogger logger, StdioLogEntry message)
33+
{
34+
ArgumentNullException.ThrowIfNull(logger);
35+
36+
logger.Log(LogLevel.Information, 0, message, exception: null, (m, _) => JsonSerializer.Serialize(m));
37+
}
38+
}
39+
40+
/// <summary>
41+
/// Represents a log entry for stdio operations.
42+
/// </summary>
43+
public class StdioLogEntry(string message, MessageType messageType, StdioLoggingContext? context)
44+
{
45+
public string Message { get; set; } = message ?? throw new ArgumentNullException(nameof(message));
46+
public MessageType MessageType { get; set; } = messageType;
47+
public string? Command { get; init; } = context?.Session.Command;
48+
public string? Direction { get; init; } = context?.Direction.ToString();
49+
public string? PluginName { get; set; }
2650
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Text.Json;
6+
7+
namespace DevProxy.Abstractions.Models;
8+
9+
/// <summary>
10+
/// Represents a mock response for stdio operations.
11+
/// </summary>
12+
public class MockStdioResponse : ICloneable
13+
{
14+
/// <summary>
15+
/// The request pattern to match against stdin.
16+
/// </summary>
17+
public MockStdioRequest? Request { get; set; }
18+
19+
/// <summary>
20+
/// The mock response to return.
21+
/// </summary>
22+
public MockStdioResponseBody? Response { get; set; }
23+
24+
public object Clone()
25+
{
26+
var json = JsonSerializer.Serialize(this);
27+
return JsonSerializer.Deserialize<MockStdioResponse>(json) ?? new MockStdioResponse();
28+
}
29+
}
30+
31+
/// <summary>
32+
/// Represents the request pattern for matching stdin.
33+
/// </summary>
34+
public class MockStdioRequest
35+
{
36+
/// <summary>
37+
/// A fragment of the stdin body to match.
38+
/// If null or empty, the mock matches any stdin (or is applied immediately on startup).
39+
/// </summary>
40+
public string? BodyFragment { get; set; }
41+
42+
/// <summary>
43+
/// The Nth occurrence to match. If null, matches every occurrence.
44+
/// </summary>
45+
public int? Nth { get; set; }
46+
}
47+
48+
/// <summary>
49+
/// Represents the mock response body for stdio.
50+
/// </summary>
51+
public class MockStdioResponseBody
52+
{
53+
/// <summary>
54+
/// The stdout content to return. Can be a string or a JSON object.
55+
/// If the value starts with @, it's treated as a file path.
56+
/// </summary>
57+
public object? Stdout { get; set; }
58+
59+
/// <summary>
60+
/// The stderr content to return. Can be a string or a JSON object.
61+
/// If the value starts with @, it's treated as a file path.
62+
/// </summary>
63+
public object? Stderr { get; set; }
64+
}

DevProxy.Abstractions/Plugins/BasePlugin.cs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ namespace DevProxy.Abstractions.Plugins;
1616

1717
public abstract class BasePlugin(
1818
ILogger logger,
19-
ISet<UrlToWatch> urlsToWatch) : IPlugin, IDisposable
19+
ISet<UrlToWatch> urlsToWatch) : IPlugin, IDisposable, IStdioPlugin
2020
{
2121
public bool Enabled { get; protected set; } = true;
2222
protected ILogger Logger { get; } = logger;
@@ -36,6 +36,7 @@ public virtual void OptionsLoaded(OptionsLoadedArgs e)
3636
{
3737
}
3838

39+
// HTTP plugin methods
3940
public virtual Task BeforeRequestAsync(ProxyRequestArgs e, CancellationToken cancellationToken)
4041
{
4142
return Task.CompletedTask;
@@ -66,6 +67,32 @@ public virtual Task MockRequestAsync(EventArgs e, CancellationToken cancellation
6667
return Task.CompletedTask;
6768
}
6869

70+
// Stdio plugin methods
71+
public virtual Task BeforeStdinAsync(StdioRequestArgs e, CancellationToken cancellationToken)
72+
{
73+
return Task.CompletedTask;
74+
}
75+
76+
public virtual Task AfterStdoutAsync(StdioResponseArgs e, CancellationToken cancellationToken)
77+
{
78+
return Task.CompletedTask;
79+
}
80+
81+
public virtual Task AfterStderrAsync(StdioResponseArgs e, CancellationToken cancellationToken)
82+
{
83+
return Task.CompletedTask;
84+
}
85+
86+
public virtual Task AfterStdioRequestLogAsync(StdioRequestLogArgs e, CancellationToken cancellationToken)
87+
{
88+
return Task.CompletedTask;
89+
}
90+
91+
public virtual Task AfterStdioRecordingStopAsync(StdioRecordingArgs e, CancellationToken cancellationToken)
92+
{
93+
return Task.CompletedTask;
94+
}
95+
6996
protected virtual void Dispose(bool disposing)
7097
{
7198
// Override in derived classes to dispose managed resources
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using DevProxy.Abstractions.Proxy;
6+
7+
namespace DevProxy.Abstractions.Plugins;
8+
9+
/// <summary>
10+
/// Interface for plugins that can intercept stdio messages.
11+
/// Plugins that implement both IPlugin and IStdioPlugin can participate
12+
/// in both HTTP proxy and stdio proxy scenarios.
13+
/// </summary>
14+
public interface IStdioPlugin
15+
{
16+
/// <summary>
17+
/// Gets the name of the plugin.
18+
/// </summary>
19+
string Name { get; }
20+
21+
/// <summary>
22+
/// Gets whether the plugin is enabled.
23+
/// </summary>
24+
bool Enabled { get; }
25+
26+
/// <summary>
27+
/// Called before stdin message is forwarded to the child process.
28+
/// Plugins can inspect, modify, or consume the message.
29+
/// Set ResponseState.HasBeenSet to true to prevent the message from being forwarded.
30+
/// </summary>
31+
/// <param name="e">The event arguments containing the stdin message.</param>
32+
/// <param name="cancellationToken">Cancellation token.</param>
33+
Task BeforeStdinAsync(StdioRequestArgs e, CancellationToken cancellationToken);
34+
35+
/// <summary>
36+
/// Called after stdout message is received from the child process.
37+
/// Plugins can inspect, modify, or consume the message.
38+
/// Set ResponseState.HasBeenSet to true to prevent the message from being forwarded.
39+
/// </summary>
40+
/// <param name="e">The event arguments containing the stdout message.</param>
41+
/// <param name="cancellationToken">Cancellation token.</param>
42+
Task AfterStdoutAsync(StdioResponseArgs e, CancellationToken cancellationToken);
43+
44+
/// <summary>
45+
/// Called after stderr message is received from the child process.
46+
/// Plugins can inspect, modify, or consume the message.
47+
/// Set ResponseState.HasBeenSet to true to prevent the message from being forwarded.
48+
/// </summary>
49+
/// <param name="e">The event arguments containing the stderr message.</param>
50+
/// <param name="cancellationToken">Cancellation token.</param>
51+
Task AfterStderrAsync(StdioResponseArgs e, CancellationToken cancellationToken);
52+
53+
/// <summary>
54+
/// Called after a stdin/stdout pair has been logged.
55+
/// Useful for recording and reporting plugins.
56+
/// </summary>
57+
/// <param name="e">The event arguments containing the request log.</param>
58+
/// <param name="cancellationToken">Cancellation token.</param>
59+
Task AfterStdioRequestLogAsync(StdioRequestLogArgs e, CancellationToken cancellationToken);
60+
61+
/// <summary>
62+
/// Called after recording has stopped.
63+
/// Useful for reporting plugins that need to process all recorded messages.
64+
/// </summary>
65+
/// <param name="e">The event arguments containing all recorded logs.</param>
66+
/// <param name="cancellationToken">Cancellation token.</param>
67+
Task AfterStdioRecordingStopAsync(StdioRecordingArgs e, CancellationToken cancellationToken);
68+
}

DevProxy.Abstractions/Proxy/IProxyLogger.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,10 @@ public enum MessageType
2626
public class LoggingContext(SessionEventArgs session)
2727
{
2828
public SessionEventArgs Session { get; } = session;
29+
}
30+
31+
public class StdioLoggingContext(StdioSession session, StdioMessageDirection direction)
32+
{
33+
public StdioSession Session { get; } = session;
34+
public StdioMessageDirection Direction { get; } = direction;
2935
}

0 commit comments

Comments
 (0)