diff --git a/src/main/kotlin/net/portswigger/mcp/tools/Tools.kt b/src/main/kotlin/net/portswigger/mcp/tools/Tools.kt index 5443eb6..d517baa 100644 --- a/src/main/kotlin/net/portswigger/mcp/tools/Tools.kt +++ b/src/main/kotlin/net/portswigger/mcp/tools/Tools.kt @@ -239,7 +239,9 @@ fun Server.registerTools(api: MontoyaApi, config: McpConfig) { return@mcpPaginatedTool sequenceOf("HTTP history access denied by Burp Suite") } - api.proxy().history().asSequence().map { truncateIfNeeded(Json.encodeToString(it.toSerializableForm())) } + val history = api.proxy().history() + val ordered = if (newestFirst) history.asReversed() else history + ordered.asSequence().map { truncateIfNeeded(Json.encodeToString(it.toSerializableForm())) } } mcpPaginatedTool("Displays items matching a specified regex within the proxy HTTP history") { @@ -251,8 +253,9 @@ fun Server.registerTools(api: MontoyaApi, config: McpConfig) { } val compiledRegex = Pattern.compile(regex) - api.proxy().history { it.contains(compiledRegex) }.asSequence() - .map { truncateIfNeeded(Json.encodeToString(it.toSerializableForm())) } + val history = api.proxy().history { it.contains(compiledRegex) } + val ordered = if (newestFirst) history.asReversed() else history + ordered.asSequence().map { truncateIfNeeded(Json.encodeToString(it.toSerializableForm())) } } mcpPaginatedTool("Displays items within the proxy WebSocket history") { @@ -263,8 +266,9 @@ fun Server.registerTools(api: MontoyaApi, config: McpConfig) { return@mcpPaginatedTool sequenceOf("WebSocket history access denied by Burp Suite") } - api.proxy().webSocketHistory().asSequence() - .map { truncateIfNeeded(Json.encodeToString(it.toSerializableForm())) } + val history = api.proxy().webSocketHistory() + val ordered = if (newestFirst) history.asReversed() else history + ordered.asSequence().map { truncateIfNeeded(Json.encodeToString(it.toSerializableForm())) } } mcpPaginatedTool("Displays items matching a specified regex within the proxy WebSocket history") { @@ -276,8 +280,43 @@ fun Server.registerTools(api: MontoyaApi, config: McpConfig) { } val compiledRegex = Pattern.compile(regex) - api.proxy().webSocketHistory { it.contains(compiledRegex) }.asSequence() - .map { truncateIfNeeded(Json.encodeToString(it.toSerializableForm())) } + val history = api.proxy().webSocketHistory { it.contains(compiledRegex) } + val ordered = if (newestFirst) history.asReversed() else history + ordered.asSequence().map { truncateIfNeeded(Json.encodeToString(it.toSerializableForm())) } + } + + mcpTool("Returns the total number of items in the proxy HTTP history, optionally filtered by regex") { + val allowed = runBlocking { + checkHistoryPermissionOrDeny(HistoryAccessType.HTTP_HISTORY, config, api, "HTTP history count") + } + if (!allowed) { + return@mcpTool "HTTP history access denied by Burp Suite" + } + + val history = if (regex != null) { + val compiledRegex = Pattern.compile(regex) + api.proxy().history { it.contains(compiledRegex) } + } else { + api.proxy().history() + } + history.size.toString() + } + + mcpTool("Returns the total number of items in the proxy WebSocket history, optionally filtered by regex") { + val allowed = runBlocking { + checkHistoryPermissionOrDeny(HistoryAccessType.WEBSOCKET_HISTORY, config, api, "WebSocket history count") + } + if (!allowed) { + return@mcpTool "WebSocket history access denied by Burp Suite" + } + + val history = if (regex != null) { + val compiledRegex = Pattern.compile(regex) + api.proxy().webSocketHistory { it.contains(compiledRegex) } + } else { + api.proxy().webSocketHistory() + } + history.size.toString() } mcpTool("Sets the state of Burp's task execution engine (paused or unpaused)") { @@ -406,18 +445,24 @@ data class SetActiveEditorContents(val text: String) data class GetScannerIssues(override val count: Int, override val offset: Int) : Paginated @Serializable -data class GetProxyHttpHistory(override val count: Int, override val offset: Int) : Paginated +data class GetProxyHttpHistory(override val count: Int, override val offset: Int, val newestFirst: Boolean = false) : Paginated @Serializable -data class GetProxyHttpHistoryRegex(val regex: String, override val count: Int, override val offset: Int) : Paginated +data class GetProxyHttpHistoryRegex(val regex: String, override val count: Int, override val offset: Int, val newestFirst: Boolean = false) : Paginated @Serializable -data class GetProxyWebsocketHistory(override val count: Int, override val offset: Int) : Paginated +data class GetProxyWebsocketHistory(override val count: Int, override val offset: Int, val newestFirst: Boolean = false) : Paginated @Serializable -data class GetProxyWebsocketHistoryRegex(val regex: String, override val count: Int, override val offset: Int) : +data class GetProxyWebsocketHistoryRegex(val regex: String, override val count: Int, override val offset: Int, val newestFirst: Boolean = false) : Paginated +@Serializable +data class GetProxyHttpHistoryCount(val regex: String? = null) + +@Serializable +data class GetProxyWebsocketHistoryCount(val regex: String? = null) + @Serializable data class GenerateCollaboratorPayload( val customData: String? = null diff --git a/src/test/kotlin/net/portswigger/mcp/tools/ToolsKtTest.kt b/src/test/kotlin/net/portswigger/mcp/tools/ToolsKtTest.kt index 5a6394a..9f1f504 100644 --- a/src/test/kotlin/net/portswigger/mcp/tools/ToolsKtTest.kt +++ b/src/test/kotlin/net/portswigger/mcp/tools/ToolsKtTest.kt @@ -14,6 +14,7 @@ import burp.api.montoya.logging.Logging import burp.api.montoya.persistence.PersistedObject import burp.api.montoya.proxy.Proxy import burp.api.montoya.proxy.ProxyHttpRequestResponse +import burp.api.montoya.proxy.ProxyWebSocketMessage import burp.api.montoya.utilities.Base64Utils import burp.api.montoya.utilities.RandomUtils import burp.api.montoya.utilities.URLUtils @@ -757,8 +758,205 @@ class ToolsKtTest { assertEquals("Reached end of items", result3.expectTextContent()) } } + + @Test + fun `get proxy history with newestFirst should return items in reverse order`() { + val proxy = mockk() + val proxyHistory = listOf( + mockk(), + mockk(), + mockk() + ) + + every { api.proxy() } returns proxy + every { proxy.history() } returns proxyHistory + + mockkStatic("net.portswigger.mcp.schema.SerializationKt") + + every { proxyHistory[0].toSerializableForm() } returns HttpRequestResponse( + request = "GET /oldest HTTP/1.1", + response = "HTTP/1.1 200 OK", + notes = "Oldest" + ) + every { proxyHistory[1].toSerializableForm() } returns HttpRequestResponse( + request = "GET /middle HTTP/1.1", + response = "HTTP/1.1 200 OK", + notes = "Middle" + ) + every { proxyHistory[2].toSerializableForm() } returns HttpRequestResponse( + request = "GET /newest HTTP/1.1", + response = "HTTP/1.1 200 OK", + notes = "Newest" + ) + + runBlocking { + val result = client.callTool( + "get_proxy_http_history", mapOf( + "count" to 2, + "offset" to 0, + "newestFirst" to true + ) + ) + + delay(100) + val text = result.expectTextContent() + assertTrue(text.contains("GET /newest"), "First item should be newest") + assertTrue(text.contains("GET /middle"), "Second item should be middle") + assertFalse(text.contains("GET /oldest"), "Oldest should not be in first page") + } + } + + @Test + fun `get proxy history with newestFirst false should return items in original order`() { + val proxy = mockk() + val proxyHistory = listOf( + mockk(), + mockk(), + mockk() + ) + + every { api.proxy() } returns proxy + every { proxy.history() } returns proxyHistory + + mockkStatic("net.portswigger.mcp.schema.SerializationKt") + + every { proxyHistory[0].toSerializableForm() } returns HttpRequestResponse( + request = "GET /oldest HTTP/1.1", + response = "HTTP/1.1 200 OK", + notes = "Oldest" + ) + every { proxyHistory[1].toSerializableForm() } returns HttpRequestResponse( + request = "GET /middle HTTP/1.1", + response = "HTTP/1.1 200 OK", + notes = "Middle" + ) + every { proxyHistory[2].toSerializableForm() } returns HttpRequestResponse( + request = "GET /newest HTTP/1.1", + response = "HTTP/1.1 200 OK", + notes = "Newest" + ) + + runBlocking { + val result = client.callTool( + "get_proxy_http_history", mapOf( + "count" to 2, + "offset" to 0, + "newestFirst" to false + ) + ) + + delay(100) + val text = result.expectTextContent() + assertTrue(text.contains("GET /oldest"), "First item should be oldest") + assertTrue(text.contains("GET /middle"), "Second item should be middle") + assertFalse(text.contains("GET /newest"), "Newest should not be in first page") + } + } } - + + @Nested + inner class HistoryCountToolsTests { + @Test + fun `get proxy http history count should return total count`() { + val proxy = mockk() + val proxyHistory = listOf( + mockk(), + mockk(), + mockk() + ) + + every { api.proxy() } returns proxy + every { proxy.history() } returns proxyHistory + + runBlocking { + val result = client.callTool( + "get_proxy_http_history_count", mapOf() + ) + + delay(100) + result.expectTextContent("3") + } + } + + @Test + fun `get proxy http history count with regex should return filtered count`() { + val proxy = mockk() + val fullHistory = listOf( + mockk(), + mockk(), + mockk() + ) + val filteredHistory = listOf( + mockk(), + mockk() + ) + + every { api.proxy() } returns proxy + every { proxy.history() } returns fullHistory + every { proxy.history(any()) } returns filteredHistory + + runBlocking { + val result = client.callTool( + "get_proxy_http_history_count", mapOf( + "regex" to "example\\.com" + ) + ) + + delay(100) + result.expectTextContent("2") + } + } + + @Test + fun `get proxy websocket history count should return total count`() { + val proxy = mockk() + val wsHistory = listOf( + mockk(), + mockk() + ) + + every { api.proxy() } returns proxy + every { proxy.webSocketHistory() } returns wsHistory + + runBlocking { + val result = client.callTool( + "get_proxy_websocket_history_count", mapOf() + ) + + delay(100) + result.expectTextContent("2") + } + } + + @Test + fun `get proxy websocket history count with regex should return filtered count`() { + val proxy = mockk() + val fullHistory = listOf( + mockk(), + mockk(), + mockk() + ) + val filteredHistory = listOf( + mockk() + ) + + every { api.proxy() } returns proxy + every { proxy.webSocketHistory() } returns fullHistory + every { proxy.webSocketHistory(any()) } returns filteredHistory + + runBlocking { + val result = client.callTool( + "get_proxy_websocket_history_count", mapOf( + "regex" to "ws://example" + ) + ) + + delay(100) + result.expectTextContent("1") + } + } + } + @Nested inner class CollaboratorToolsTests { private val collaborator = mockk()