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