using System.Globalization; using System.Text.Json; using Agent.Core.Localization; using Agent.Mcp; using System.Collections.Concurrent; namespace Agent.App.Logging; public sealed class McpHostLogObserver : IMcpHostObserver, IDisposable { private readonly RollingFileLog _application; private readonly RollingFileWriter? _http; private readonly LocaleCatalog _locale; private readonly string _logDirectory; private readonly ConcurrentDictionary _observedConnections = new(StringComparer.Ordinal); public McpHostLogObserver(RollingFileLog application, LocaleCatalog locale, string logDirectory, bool httpRequests, long maxBytes, int retainedFiles) { _application = application; _locale = locale; _logDirectory = logDirectory; if (httpRequests) _http = new RollingFileWriter(Path.Combine(logDirectory, "http.jsonl"), maxBytes, retainedFiles); } public void Ready(McpReadyInfo info) { string message = info.Transport == "http" ? _locale.Format("app.ready.http", new Dictionary { ["endpoint"] = info.Endpoint, ["protocol"] = info.ProtocolVersion, ["stateless"] = info.Stateless.ToString().ToLowerInvariant(), ["fallback"] = info.LegacyFallback.ToString().ToLowerInvariant(), ["pid"] = info.ProcessId, ["logs"] = _logDirectory }) : _locale.Format("app.ready.stdio", new Dictionary { ["protocol"] = info.ProtocolVersion, ["pid"] = info.ProcessId, ["logs"] = _logDirectory }); _application.Operational(message); } public void HttpRequestStarted(McpHttpRequestStartInfo info) { if (_observedConnections.TryAdd(info.ConnectionId, 0)) _application.Information($"HTTP connection observed connection={info.ConnectionId} remote={info.RemoteIp}:{info.RemotePort}"); _application.Information($"HTTP request started connection={info.ConnectionId} trace={info.TraceId} remote={info.RemoteIp}:{info.RemotePort} method={info.Method} path={info.Path}"); } public void HttpRequest(McpHttpRequestInfo info) { _application.Information(string.Create(CultureInfo.InvariantCulture, $"HTTP request completed connection={info.ConnectionId} remote={info.RemoteIp}:{info.RemotePort} method={info.Method} path={info.Path} status={info.Status} duration_ms={info.DurationMs:F3}")); _http?.WriteLine(JsonSerializer.Serialize(new { timestamp = info.Timestamp, connection_id = info.ConnectionId, trace_id = info.TraceId, remote_ip = info.RemoteIp, remote_port = info.RemotePort, local_ip = info.LocalIp, local_port = info.LocalPort, method = info.Method, path = info.Path, status = info.Status, duration_ms = info.DurationMs, content_type = info.ContentType, content_length = info.ContentLength, user_agent = info.UserAgent, mcp_protocol_version = NullIfEmpty(info.ProtocolVersion), mcp_method = NullIfEmpty(info.McpMethod), mcp_name = NullIfEmpty(info.McpName) })); } private static string? NullIfEmpty(string? value) => string.IsNullOrWhiteSpace(value) ? null : value; public void Dispose() => _http?.Dispose(); }