From f6604a3e1d41fc1e4b9f0262cf8c1e40eeff14fa Mon Sep 17 00:00:00 2001 From: Roman <329079+romanr@users.noreply.github.com> Date: Wed, 11 Mar 2026 02:17:32 +1100 Subject: [PATCH 1/2] Log request/response bodies and added version Improve observability and expose the tool version to clear the confusion. - Enhance HTTPHandler to capture an optional client name (from Mcp-Client-Name header or JSON request body) and include it in request/response metadata. - Added detailed request and response body logging with a 4096-byte preview, byte counts, truncated marker, and related metadata (id, method, path, remote, client, session, status). - Introduce helper functions for body logging and client name extraction. - Added Version in startup stdout log. - Added --version cli parameter --- Sources/XcodeMCPProxy/HTTPHandler.swift | 135 +++++++++++++++++- Sources/XcodeMCPProxy/ProxyServer.swift | 2 +- Sources/XcodeMCPProxy/Version.swift | 5 + .../XcodeMCPProxyCLICommand.swift | 10 ++ .../XcodeMCPProxyServerCommand.swift | 15 ++ 5 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 Sources/XcodeMCPProxy/Version.swift diff --git a/Sources/XcodeMCPProxy/HTTPHandler.swift b/Sources/XcodeMCPProxy/HTTPHandler.swift index 2cf13df..87ed024 100644 --- a/Sources/XcodeMCPProxy/HTTPHandler.swift +++ b/Sources/XcodeMCPProxy/HTTPHandler.swift @@ -14,6 +14,21 @@ final class HTTPHandler: ChannelInboundHandler, Sendable { let method: String let path: String let remoteAddress: String? + let clientName: String? + + func withClientName(_ name: String?) -> RequestLogContext { + guard let name = name?.trimmingCharacters(in: .whitespacesAndNewlines), + !name.isEmpty else { + return self + } + return RequestLogContext( + id: id, + method: method, + path: path, + remoteAddress: remoteAddress, + clientName: name + ) + } } private struct State: Sendable { @@ -28,6 +43,7 @@ final class HTTPHandler: ChannelInboundHandler, Sendable { private let config: ProxyConfig private let sessionManager: any SessionManaging private let logger: Logger = ProxyLogging.make("http") + private let maxLoggedBodyBytes = 4096 init(config: ProxyConfig, sessionManager: any SessionManaging) { self.config = config @@ -107,7 +123,8 @@ final class HTTPHandler: ChannelInboundHandler, Sendable { id: UUID().uuidString, method: head.method.rawValue, path: path, - remoteAddress: remoteAddressString(for: context.channel) + remoteAddress: remoteAddressString(for: context.channel), + clientName: nil ) logRequest(requestLog) @@ -314,6 +331,10 @@ final class HTTPHandler: ChannelInboundHandler, Sendable { return } + let clientName = clientName(from: head.headers, bodyData: bodyData) + let requestLog = requestLog.withClientName(clientName) + logRequestBody(bodyData, requestLog: requestLog) + let headerSessionId = HTTPRequestValidator.sessionId(from: head.headers) let headerSessionExists = headerSessionId.map { sessionManager.hasSession(id: $0) } ?? false @@ -743,6 +764,7 @@ final class HTTPHandler: ChannelInboundHandler, Sendable { ) return } + logResponseBody(data, requestLog: requestLog, status: .ok, sessionId: sessionId) logResponse(requestLog, status: .ok, sessionId: sessionId) } @@ -839,6 +861,9 @@ final class HTTPHandler: ChannelInboundHandler, Sendable { } private func sendJSON(on channel: Channel, buffer: ByteBuffer, keepAlive: Bool, sessionId: String, requestLog: RequestLogContext) { + if let data = buffer.getData(at: buffer.readerIndex, length: buffer.readableBytes) { + logResponseBody(data, requestLog: requestLog, status: .ok, sessionId: sessionId) + } logResponse(requestLog, status: .ok, sessionId: sessionId) MCPResponseEmitter.sendJSON( on: channel, @@ -855,6 +880,7 @@ final class HTTPHandler: ChannelInboundHandler, Sendable { sessionId: String?, requestLog: RequestLogContext ) { + logResponseBody(data, requestLog: requestLog, status: .ok, sessionId: sessionId) logResponse(requestLog, status: .ok, sessionId: sessionId) var buffer = channel.allocator.buffer(capacity: data.count) buffer.writeBytes(data) @@ -874,6 +900,7 @@ final class HTTPHandler: ChannelInboundHandler, Sendable { sessionId: String?, requestLog: RequestLogContext ) { + logResponseBody(body, requestLog: requestLog, status: status, sessionId: sessionId) logResponse(requestLog, status: status, sessionId: sessionId) MCPResponseEmitter.sendPlain( on: channel, @@ -1024,6 +1051,9 @@ final class HTTPHandler: ChannelInboundHandler, Sendable { if let remote = request.remoteAddress { metadata["remote"] = .string(remote) } + if let clientName = request.clientName { + metadata["client"] = .string(clientName) + } logger.info("HTTP request", metadata: metadata) } @@ -1037,12 +1067,115 @@ final class HTTPHandler: ChannelInboundHandler, Sendable { if let remote = request.remoteAddress { metadata["remote"] = .string(remote) } + if let clientName = request.clientName { + metadata["client"] = .string(clientName) + } if let sessionId { metadata["session"] = .string(sessionId) } logger.info("HTTP response", metadata: metadata) } + private func logRequestBody(_ data: Data, requestLog: RequestLogContext) { + logBody( + label: "HTTP request body", + data: data, + requestLog: requestLog, + status: nil, + sessionId: nil + ) + } + + private func logResponseBody(_ data: Data, requestLog: RequestLogContext, status: HTTPResponseStatus, sessionId: String?) { + logBody( + label: "HTTP response body", + data: data, + requestLog: requestLog, + status: status, + sessionId: sessionId + ) + } + + private func logResponseBody(_ body: String, requestLog: RequestLogContext, status: HTTPResponseStatus, sessionId: String?) { + let data = Data(body.utf8) + logBody( + label: "HTTP response body", + data: data, + requestLog: requestLog, + status: status, + sessionId: sessionId + ) + } + + private func logBody( + label: String, + data: Data, + requestLog: RequestLogContext, + status: HTTPResponseStatus?, + sessionId: String? + ) { + var metadata: Logger.Metadata = [ + "id": .string(requestLog.id), + "method": .string(requestLog.method), + "path": .string(requestLog.path), + "bytes": .string("\(data.count)"), + ] + if let remote = requestLog.remoteAddress { + metadata["remote"] = .string(remote) + } + if let clientName = requestLog.clientName { + metadata["client"] = .string(clientName) + } + if let status { + metadata["status"] = .string("\(status.code)") + } + if let sessionId { + metadata["session"] = .string(sessionId) + } + let previewData = data.prefix(maxLoggedBodyBytes) + let previewText = String(decoding: previewData, as: UTF8.self) + metadata["preview"] = .string(previewText) + if data.count > maxLoggedBodyBytes { + metadata["truncated"] = .string("true") + } + logger.info(Logger.Message(stringLiteral: label), metadata: metadata) + } + + private func clientName(from headers: HTTPHeaders, bodyData: Data) -> String? { + if let header = headers.first(name: "Mcp-Client-Name")?.trimmingCharacters(in: .whitespacesAndNewlines), + !header.isEmpty { + return header + } + return clientName(from: bodyData) + } + + private func clientName(from bodyData: Data) -> String? { + guard let json = try? JSONSerialization.jsonObject(with: bodyData, options: []) else { + return nil + } + if let object = json as? [String: Any] { + return clientName(from: object) + } + if let array = json as? [Any] { + for item in array { + if let object = item as? [String: Any], let name = clientName(from: object) { + return name + } + } + } + return nil + } + + private func clientName(from object: [String: Any]) -> String? { + guard let params = object["params"] as? [String: Any], + let clientInfo = params["clientInfo"] as? [String: Any], + let name = clientInfo["name"] as? String else { + return nil + } + let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines) + return trimmed.isEmpty ? nil : trimmed + } + private func remoteAddressString(for channel: Channel) -> String? { guard let address = channel.remoteAddress else { return nil diff --git a/Sources/XcodeMCPProxy/ProxyServer.swift b/Sources/XcodeMCPProxy/ProxyServer.swift index d42a0ef..c0ee3cf 100644 --- a/Sources/XcodeMCPProxy/ProxyServer.swift +++ b/Sources/XcodeMCPProxy/ProxyServer.swift @@ -27,7 +27,7 @@ public final class ProxyServer { let (host, port) = resolvedListenAddress(for: channel) let displayHost = config.listenHost == "localhost" ? "localhost" : host writeDiscovery(resolvedHost: host, port: port) - logger.info("Xcode MCP proxy listening on http://\(displayHost):\(port)") + logger.info("Xcode MCP proxy \(Version.current) listening on http://\(displayHost):\(port)") return (host, port) } diff --git a/Sources/XcodeMCPProxy/Version.swift b/Sources/XcodeMCPProxy/Version.swift new file mode 100644 index 0000000..84ba4b6 --- /dev/null +++ b/Sources/XcodeMCPProxy/Version.swift @@ -0,0 +1,5 @@ +import Foundation + +public enum Version { + public static let current = "0.4.1" +} diff --git a/Sources/XcodeMCPProxyCommands/XcodeMCPProxyCLICommand.swift b/Sources/XcodeMCPProxyCommands/XcodeMCPProxyCLICommand.swift index 10b1bd7..15edff0 100644 --- a/Sources/XcodeMCPProxyCommands/XcodeMCPProxyCLICommand.swift +++ b/Sources/XcodeMCPProxyCommands/XcodeMCPProxyCLICommand.swift @@ -19,6 +19,7 @@ package struct CLICommandLogSink { package struct CLICommandInvocation { package var showHelp = false + package var showVersion = false package var usesRemovedURLHelper = false package var hasExplicitURL = false package var hasStdioFlag = false @@ -93,6 +94,11 @@ package struct XcodeMCPProxyCLICommand { return 0 } + if invocation.showVersion { + dependencies.stdout("xcode-mcp-proxy \(Version.current)") + return 0 + } + if invocation.usesRemovedURLHelper { logSink.error( "url helper mode was removed; configure your HTTP client with a concrete URL (default: http://localhost:8765/mcp)." @@ -158,6 +164,9 @@ package struct XcodeMCPProxyCLICommand { case "-h", "--help": invocation.showHelp = true index += 1 + case "--version": + invocation.showVersion = true + index += 1 case "url" where index == 1: invocation.usesRemovedURLHelper = true index += 1 @@ -259,6 +268,7 @@ package struct XcodeMCPProxyCLICommand { Options: --request-timeout seconds Request timeout (default: 300, 0 disables) --url url Explicit upstream URL (default: env/discovery/http://localhost:8765/mcp) + --version Show version -h, --help Show help Environment: diff --git a/Sources/XcodeMCPProxyCommands/XcodeMCPProxyServerCommand.swift b/Sources/XcodeMCPProxyCommands/XcodeMCPProxyServerCommand.swift index d4785c1..10c8ed2 100644 --- a/Sources/XcodeMCPProxyCommands/XcodeMCPProxyServerCommand.swift +++ b/Sources/XcodeMCPProxyCommands/XcodeMCPProxyServerCommand.swift @@ -7,6 +7,7 @@ extension ProxyServer: ProxyServerCommandServer {} package struct ProxyServerOptions { package var forwardedArgs: [String] package var showHelp: Bool + package var showVersion: Bool package var hasListenFlag: Bool package var hasHostFlag: Bool package var hasPortFlag: Bool @@ -18,6 +19,7 @@ package struct ProxyServerOptions { package init( forwardedArgs: [String], showHelp: Bool, + showVersion: Bool, hasListenFlag: Bool, hasHostFlag: Bool, hasPortFlag: Bool, @@ -28,6 +30,7 @@ package struct ProxyServerOptions { ) { self.forwardedArgs = forwardedArgs self.showHelp = showHelp + self.showVersion = showVersion self.hasListenFlag = hasListenFlag self.hasHostFlag = hasHostFlag self.hasPortFlag = hasPortFlag @@ -109,6 +112,10 @@ package struct XcodeMCPProxyServerCommand { dependencies.stdout(Self.serverUsage()) return 0 } + if options.showVersion { + dependencies.stdout("xcode-mcp-proxy-server \(Version.current)") + return 0 + } Self.applyDefaults( from: environment, to: &options, @@ -167,6 +174,7 @@ package struct XcodeMCPProxyServerCommand { package static func parseOptions(args: [String]) throws -> ProxyServerOptions { var forwarded: [String] = [] var showHelp = false + var showVersion = false var hasListen = false var hasHost = false var hasPort = false @@ -198,6 +206,7 @@ package struct XcodeMCPProxyServerCommand { return ProxyServerOptions( forwardedArgs: forwarded, showHelp: showHelp, + showVersion: showVersion, hasListenFlag: hasListen, hasHostFlag: hasHost, hasPortFlag: hasPort, @@ -206,6 +215,10 @@ package struct XcodeMCPProxyServerCommand { forceRestart: forceRestart, dryRun: dryRun ) + case "--version": + showVersion = true + index += 1 + continue case "--dry-run": dryRun = true index += 1 @@ -254,6 +267,7 @@ package struct XcodeMCPProxyServerCommand { return ProxyServerOptions( forwardedArgs: forwarded, showHelp: showHelp, + showVersion: showVersion, hasListenFlag: hasListen, hasHostFlag: hasHost, hasPortFlag: hasPort, @@ -319,6 +333,7 @@ package struct XcodeMCPProxyServerCommand { --lazy-init --force-restart --dry-run + --version -h, --help Notes: From 781e7a8cb59b4148af3c5487940846f416ac0b02 Mon Sep 17 00:00:00 2001 From: Roman Roan <329079+romanr@users.noreply.github.com> Date: Thu, 12 Mar 2026 07:17:10 +1100 Subject: [PATCH 2/2] Update Sources/XcodeMCPProxy/Version.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Sources/XcodeMCPProxy/Version.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sources/XcodeMCPProxy/Version.swift b/Sources/XcodeMCPProxy/Version.swift index 84ba4b6..7c9399c 100644 --- a/Sources/XcodeMCPProxy/Version.swift +++ b/Sources/XcodeMCPProxy/Version.swift @@ -1,5 +1,3 @@ -import Foundation - public enum Version { public static let current = "0.4.1" }