From ee6594af8b62a69b2b77f000c52a83fc7203c5e8 Mon Sep 17 00:00:00 2001 From: Mayank Dhawan Date: Tue, 3 Mar 2026 13:40:36 +0530 Subject: [PATCH] fix: normalize literal \r\n escape sequences in HTTP content from MCP clients MCP clients pass \r\n as literal 4-character text sequences in tool parameters rather than actual CR+LF bytes. The existing normalization in send_http1_request only handled actual CR/LF characters, causing malformed HTTP requests that strict servers (e.g. Apache-Coyote) reject with 400 Bad Request. This commit: - Adds normalizeHttpContent() helper that handles both literal escape sequences and actual line endings - Applies normalization to send_http1_request (replacing the existing incomplete fix) - Applies normalization to create_repeater_tab (previously had no normalization at all) - Applies normalization to send_to_intruder (previously had no normalization at all) Relates to #51 --- .../kotlin/net/portswigger/mcp/tools/Tools.kt | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/net/portswigger/mcp/tools/Tools.kt b/src/main/kotlin/net/portswigger/mcp/tools/Tools.kt index 5443eb6..a47ca7d 100644 --- a/src/main/kotlin/net/portswigger/mcp/tools/Tools.kt +++ b/src/main/kotlin/net/portswigger/mcp/tools/Tools.kt @@ -42,6 +42,25 @@ private fun truncateIfNeeded(serialized: String): String { } } +/** + * Normalizes HTTP content line endings from MCP clients. + * + * MCP clients (e.g. Claude Code) pass \r\n as literal escape sequences in JSON + * tool parameters, which arrive as the 4-character text sequences backslash-r + * and backslash-n rather than actual CR (0x0D) and LF (0x0A) bytes. + * This produces malformed HTTP that strict servers (e.g. Apache-Coyote) reject + * with 400 Bad Request. + * + * This function converts both literal escape sequences and actual line endings + * into proper HTTP CRLF line termination. + */ +private fun normalizeHttpContent(content: String): String = content + .replace("\\r\\n", "\n") // Literal \r\n escape sequences → LF + .replace("\\n", "\n") // Remaining literal \n → LF + .replace("\\r", "") // Remaining literal \r → remove + .replace("\r", "") // Actual CR → remove + .replace("\n", "\r\n") // All LF → proper CRLF + fun Server.registerTools(api: MontoyaApi, config: McpConfig) { mcpTool("Issues an HTTP/1.1 request and returns the response.") { @@ -55,7 +74,7 @@ fun Server.registerTools(api: MontoyaApi, config: McpConfig) { api.logging().logToOutput("MCP HTTP/1.1 request: $targetHostname:$targetPort") - val fixedContent = content.replace("\r", "").replace("\n", "\r\n") + val fixedContent = normalizeHttpContent(content) val request = HttpRequest.httpRequest(toMontoyaService(), fixedContent) val response = api.http().sendRequest(request) @@ -115,12 +134,14 @@ fun Server.registerTools(api: MontoyaApi, config: McpConfig) { } mcpTool("Creates a new Repeater tab with the specified HTTP request and optional tab name. Make sure to use carriage returns appropriately.") { - val request = HttpRequest.httpRequest(toMontoyaService(), content) + val fixedContent = normalizeHttpContent(content) + val request = HttpRequest.httpRequest(toMontoyaService(), fixedContent) api.repeater().sendToRepeater(request, tabName) } mcpTool("Sends an HTTP request to Intruder with the specified HTTP request and optional tab name. Make sure to use carriage returns appropriately.") { - val request = HttpRequest.httpRequest(toMontoyaService(), content) + val fixedContent = normalizeHttpContent(content) + val request = HttpRequest.httpRequest(toMontoyaService(), fixedContent) api.intruder().sendToIntruder(request, tabName) } @@ -426,4 +447,4 @@ data class GenerateCollaboratorPayload( @Serializable data class GetCollaboratorInteractions( val payloadId: String? = null -) \ No newline at end of file +)