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