From 91e18bbb0eadc8ef7de36a642ad044683449fb24 Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Fri, 6 Feb 2026 15:10:04 +0900 Subject: [PATCH] Use HTTP forwarder for stdio-jsonrpc2 Replace the old StdioServer-based stdio JSON-RPC flow with an in-process HTTP forwarder. Program.cs now reads raw stdin, posts payloads to the configured ResourceServerPrefix + "jsonrpc2" using HttpClient (configurable timeout), writes HTTP response bytes back to stdout, and includes improved logging and SafePreviewUtf8 for safe payload previews. StdioServer.cs and its project entry were removed, and Resources.resx HttpClientTimeout was increased to 300 (Designer version updated). This centralizes stdio handling and HTTP forwarding with better error handling and diagnostics. --- .../WelsonJS.Launcher/Program.cs | 135 ++++++++--- .../Properties/Resources.Designer.cs | 4 +- .../Properties/Resources.resx | 2 +- .../WelsonJS.Launcher/StdioServer.cs | 209 ------------------ .../WelsonJS.Launcher.csproj | 1 - 5 files changed, 113 insertions(+), 238 deletions(-) delete mode 100644 WelsonJS.Augmented/WelsonJS.Launcher/StdioServer.cs diff --git a/WelsonJS.Augmented/WelsonJS.Launcher/Program.cs b/WelsonJS.Augmented/WelsonJS.Launcher/Program.cs index 711dd49..83c6630 100644 --- a/WelsonJS.Augmented/WelsonJS.Launcher/Program.cs +++ b/WelsonJS.Augmented/WelsonJS.Launcher/Program.cs @@ -11,9 +11,12 @@ using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; using System.Reflection; using System.Text; using System.Threading; +using System.Threading.Tasks; using System.Windows.Forms; namespace WelsonJS.Launcher @@ -61,10 +64,11 @@ namespace WelsonJS.Launcher return; } - // if use stdio JSON-RPC 2.0 mode + // if use the stdio-jsonrpc2 forwarder if (HasArg(args, "--stdio-jsonrpc2")) { - RunJsonRpc2StdioServer(); + _logger.Info("Starting in the stdio-jsonrpc2 forwarder..."); + ProcessStdioJsonRpc2().GetAwaiter().GetResult(); return; } @@ -102,40 +106,121 @@ namespace WelsonJS.Launcher return false; } - private static void RunJsonRpc2StdioServer() + private async static Task ProcessStdioJsonRpc2() { - var server = new StdioServer(async (payload, ct) => + string serverPrefix = GetAppConfig("ResourceServerPrefix"); + string endpoint = $"{serverPrefix}jsonrpc2"; + int timeout = int.TryParse(GetAppConfig("HttpClientTimeout"), out timeout) ? timeout : 300; + + var http = new HttpClient { - var dispatcher = new JsonRpc2Dispatcher(_logger); - var body = Encoding.UTF8.GetString(payload); + Timeout = TimeSpan.FromSeconds(timeout) + }; - using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(300))) + using (var stdin = Console.OpenStandardInput()) + using (var stdout = Console.OpenStandardOutput()) + { + var buffer = new byte[8192]; + + while (true) { - string result = await dispatcher.HandleAsync( - body, - async (method, ser, _ct) => + int read; + try + { + read = await stdin.ReadAsync(buffer, 0, buffer.Length); + } + catch (Exception ex) + { + _logger.Error("[stdio] stdin read failed", ex); + break; + } + + if (read <= 0) + { + _logger.Info("[stdio] EOF received, exiting loop"); + break; + } + + byte[] payload = new byte[read]; + Buffer.BlockCopy(buffer, 0, payload, 0, read); + + _logger.Debug($"[stdio] recv {payload.Length} bytes"); + _logger.Debug($"[stdio] payload preview: {SafePreviewUtf8(payload, 512)}"); + + try + { + using (var content = new ByteArrayContent(payload)) { - switch (method) + // Content-Type: application/json + content.Headers.ContentType = + new MediaTypeHeaderValue("application/json") + { + CharSet = "utf-8" + }; + + _logger.Debug($"[http] POST {endpoint}"); + + using (var response = await http.PostAsync(endpoint, content)) { - case "tools/list": - return Encoding.UTF8.GetString(ResourceServer.GetResource("McpToolList.json")); + _logger.Info( + $"[http] status={(int)response.StatusCode} {response.ReasonPhrase}"); - case "tools/call": - // TODO: implement tool call handling - return string.Empty; + foreach (var h in response.Headers) + _logger.Debug($"[http] H {h.Key}: {string.Join(", ", h.Value)}"); - default: - return string.Empty; + foreach (var h in response.Content.Headers) + _logger.Debug($"[http] HC {h.Key}: {string.Join(", ", h.Value)}"); + + byte[] responseBytes = + await response.Content.ReadAsByteArrayAsync(); + + _logger.Debug($"[http] body {responseBytes.Length} bytes"); + + _logger.Debug( + $"[http] body preview: {SafePreviewUtf8(responseBytes, 2048)}"); + + await stdout.WriteAsync(responseBytes, 0, responseBytes.Length); + await stdout.FlushAsync(); } - }, - cts.Token); - - // Fix: Convert string result to byte[] before returning - return Encoding.UTF8.GetBytes(result); + } + } + catch (TaskCanceledException ex) + { + _logger.Error("[http] request timed out or canceled", ex); + } + catch (HttpRequestException ex) + { + _logger.Error("[http] request failed", ex); + } + catch (Exception ex) + { + _logger.Error("[http] unexpected error", ex); + } } - }); + } + } - server.Run(); + private static string SafePreviewUtf8(byte[] bytes, int maxBytes) + { + if (bytes == null || bytes.Length == 0) + return "(empty)"; + + try + { + int len = Math.Min(bytes.Length, maxBytes); + string s = Encoding.UTF8.GetString(bytes, 0, len); + + s = s.Replace("\r", "\\r").Replace("\n", "\\n"); + + if (bytes.Length > maxBytes) + s += $"...(truncated, total {bytes.Length} bytes)"; + + return s; + } + catch + { + return "(binary / non-utf8 response)"; + } } private static void InitializeAssemblyLoader() diff --git a/WelsonJS.Augmented/WelsonJS.Launcher/Properties/Resources.Designer.cs b/WelsonJS.Augmented/WelsonJS.Launcher/Properties/Resources.Designer.cs index 4edcfc2..ddc28cb 100644 --- a/WelsonJS.Augmented/WelsonJS.Launcher/Properties/Resources.Designer.cs +++ b/WelsonJS.Augmented/WelsonJS.Launcher/Properties/Resources.Designer.cs @@ -19,7 +19,7 @@ namespace WelsonJS.Launcher.Properties { // 클래스에서 자동으로 생성되었습니다. // 멤버를 추가하거나 제거하려면 .ResX 파일을 편집한 다음 /str 옵션을 사용하여 ResGen을 // 다시 실행하거나 VS 프로젝트를 다시 빌드하십시오. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resources { @@ -197,7 +197,7 @@ namespace WelsonJS.Launcher.Properties { } /// - /// 90과(와) 유사한 지역화된 문자열을 찾습니다. + /// 300과(와) 유사한 지역화된 문자열을 찾습니다. /// internal static string HttpClientTimeout { get { diff --git a/WelsonJS.Augmented/WelsonJS.Launcher/Properties/Resources.resx b/WelsonJS.Augmented/WelsonJS.Launcher/Properties/Resources.resx index bb52cbb..d51141b 100644 --- a/WelsonJS.Augmented/WelsonJS.Launcher/Properties/Resources.resx +++ b/WelsonJS.Augmented/WelsonJS.Launcher/Properties/Resources.resx @@ -188,7 +188,7 @@ https://catswords.blob.core.windows.net/welsonjs/blob.config.xml - 90 + 300 diff --git a/WelsonJS.Augmented/WelsonJS.Launcher/StdioServer.cs b/WelsonJS.Augmented/WelsonJS.Launcher/StdioServer.cs deleted file mode 100644 index ac7524c..0000000 --- a/WelsonJS.Augmented/WelsonJS.Launcher/StdioServer.cs +++ /dev/null @@ -1,209 +0,0 @@ -// StdioServer.cs -// Minimal stdio server (Content-Length framing) with delegate-based extension. -// - Handles ONLY stdio framing (read/write message boundaries) -// - No JSON parsing/formatting -// - Sequential processing -// - Cancellation via Ctrl+C, and EOF handling -// -// Delegate contract: -// - input: raw UTF-8 payload bytes (exactly Content-Length) -// - output: raw UTF-8 payload bytes to write (or null/empty to write nothing) -// -// Typical use: plug JSON-RPC/MCP dispatcher outside of this class. -using System; -using System.Globalization; -using System.IO; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace WelsonJS.Launcher -{ - public sealed class StdioServer - { - public delegate Task Handler(byte[] payload, CancellationToken ct); - - private readonly Stream _inStream; - private readonly Stream _outStream; - private readonly Handler _handler; - - public StdioServer(Handler handler) - { - if (handler == null) - throw new ArgumentNullException("handler"); - - _handler = handler; - _inStream = Console.OpenStandardInput(); - _outStream = Console.OpenStandardOutput(); - } - - public void Run() - { - using (var cts = new CancellationTokenSource()) - { - Console.CancelKeyPress += (s, e) => - { - e.Cancel = true; - cts.Cancel(); - }; - - RunAsync(cts.Token).GetAwaiter().GetResult(); - } - } - - public async Task RunAsync(CancellationToken ct) - { - while (!ct.IsCancellationRequested) - { - byte[] payload; - - // 1) read one framed message (blocks here waiting for stdin) - try - { - payload = await ReadOneAsync(ct).ConfigureAwait(false); - if (payload == null) return; // EOF => exit - } - catch (OperationCanceledException) - { - return; - } - catch - { - // framing broken or stream error => stop (or continue if you want resync) - return; - } - - // 2) handle + write response (never kill the loop on handler failure) - try - { - var resp = await _handler(payload, ct).ConfigureAwait(false); - if (resp == null) resp = new byte[0]; - - await WriteOneAsync(resp, ct).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - return; - } - catch - { - // keep listening even if handler fails - // optionally write empty response so client doesn't hang waiting - try { await WriteOneAsync(new byte[0], ct).ConfigureAwait(false); } catch { } - continue; - } - } - } - - private async Task ReadOneAsync(CancellationToken ct) - { - // Read headers until \r\n\r\n (blocks on stdin) - string headers = await ReadHeadersAsync(ct).ConfigureAwait(false); - if (headers == null) return null; // EOF - - int contentLength = ParseContentLength(headers); - if (contentLength < 0) throw new InvalidDataException("Missing Content-Length"); - - return await ReadExactAsync(_inStream, contentLength, ct).ConfigureAwait(false); - } - - private async Task WriteOneAsync(byte[] payload, CancellationToken ct) - { - if (payload == null) payload = new byte[0]; - - string header = "Content-Length: " + payload.Length.ToString(CultureInfo.InvariantCulture) + "\r\n\r\n"; - byte[] headerBytes = Encoding.ASCII.GetBytes(header); - - await _outStream.WriteAsync(headerBytes, 0, headerBytes.Length, ct).ConfigureAwait(false); - if (payload.Length > 0) - await _outStream.WriteAsync(payload, 0, payload.Length, ct).ConfigureAwait(false); - - await _outStream.FlushAsync(ct).ConfigureAwait(false); - } - - private async Task ReadHeadersAsync(CancellationToken ct) - { - // read byte-by-byte until CRLFCRLF - var buf = new byte[4096]; - int len = 0; - - while (true) - { - ct.ThrowIfCancellationRequested(); - - int b = await ReadByteAsync(_inStream, ct).ConfigureAwait(false); - if (b < 0) - { - if (len == 0) return null; // clean EOF - throw new EndOfStreamException("EOF while reading headers"); - } - - if (len == buf.Length) - { - // grow - var nb = new byte[buf.Length * 2]; - Buffer.BlockCopy(buf, 0, nb, 0, buf.Length); - buf = nb; - } - - buf[len++] = (byte)b; - - if (len >= 4 && - buf[len - 4] == 13 && - buf[len - 3] == 10 && - buf[len - 2] == 13 && - buf[len - 1] == 10) - { - return Encoding.ASCII.GetString(buf, 0, len); - } - } - } - - private static int ParseContentLength(string headers) - { - // minimal parser: Content-Length: N - var lines = headers.Split(new[] { "\r\n" }, StringSplitOptions.None); - for (int i = 0; i < lines.Length; i++) - { - var line = lines[i]; - int colon = line.IndexOf(':'); - if (colon <= 0) continue; - - var name = line.Substring(0, colon).Trim(); - if (!name.Equals("Content-Length", StringComparison.OrdinalIgnoreCase)) continue; - - var val = line.Substring(colon + 1).Trim(); - int n; - if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out n)) - return n; - } - return -1; - } - - private static async Task ReadExactAsync(Stream s, int nBytes, CancellationToken ct) - { - if (nBytes == 0) return new byte[0]; - - var buf = new byte[nBytes]; - int read = 0; - - while (read < nBytes) - { - ct.ThrowIfCancellationRequested(); - int n = await s.ReadAsync(buf, read, nBytes - read, ct).ConfigureAwait(false); - if (n <= 0) throw new EndOfStreamException("EOF while reading payload"); - read += n; - } - - return buf; - } - - private static async Task ReadByteAsync(Stream s, CancellationToken ct) - { - var b = new byte[1]; - int n = await s.ReadAsync(b, 0, 1, ct).ConfigureAwait(false); - if (n <= 0) return -1; - return b[0]; - } - } -} diff --git a/WelsonJS.Augmented/WelsonJS.Launcher/WelsonJS.Launcher.csproj b/WelsonJS.Augmented/WelsonJS.Launcher/WelsonJS.Launcher.csproj index 24970c3..493029e 100644 --- a/WelsonJS.Augmented/WelsonJS.Launcher/WelsonJS.Launcher.csproj +++ b/WelsonJS.Augmented/WelsonJS.Launcher/WelsonJS.Launcher.csproj @@ -133,7 +133,6 @@ GlobalSettingsForm.cs - EnvForm.cs