Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 56 additions & 11 deletions src/main/kotlin/net/portswigger/mcp/tools/Tools.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<GetProxyHttpHistoryRegex>("Displays items matching a specified regex within the proxy HTTP history") {
Expand All @@ -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<GetProxyWebsocketHistory>("Displays items within the proxy WebSocket history") {
Expand All @@ -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<GetProxyWebsocketHistoryRegex>("Displays items matching a specified regex within the proxy WebSocket history") {
Expand All @@ -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<GetProxyHttpHistoryCount>("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<GetProxyWebsocketHistoryCount>("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<SetTaskExecutionEngineState>("Sets the state of Burp's task execution engine (paused or unpaused)") {
Expand Down Expand Up @@ -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
Expand Down
200 changes: 199 additions & 1 deletion src/test/kotlin/net/portswigger/mcp/tools/ToolsKtTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<Proxy>()
val proxyHistory = listOf(
mockk<ProxyHttpRequestResponse>(),
mockk<ProxyHttpRequestResponse>(),
mockk<ProxyHttpRequestResponse>()
)

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<Proxy>()
val proxyHistory = listOf(
mockk<ProxyHttpRequestResponse>(),
mockk<ProxyHttpRequestResponse>(),
mockk<ProxyHttpRequestResponse>()
)

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<Proxy>()
val proxyHistory = listOf(
mockk<ProxyHttpRequestResponse>(),
mockk<ProxyHttpRequestResponse>(),
mockk<ProxyHttpRequestResponse>()
)

every { api.proxy() } returns proxy
every { proxy.history() } returns proxyHistory

runBlocking {
val result = client.callTool(
"get_proxy_http_history_count", mapOf<String, Any>()
)

delay(100)
result.expectTextContent("3")
}
}

@Test
fun `get proxy http history count with regex should return filtered count`() {
val proxy = mockk<Proxy>()
val fullHistory = listOf(
mockk<ProxyHttpRequestResponse>(),
mockk<ProxyHttpRequestResponse>(),
mockk<ProxyHttpRequestResponse>()
)
val filteredHistory = listOf(
mockk<ProxyHttpRequestResponse>(),
mockk<ProxyHttpRequestResponse>()
)

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<Proxy>()
val wsHistory = listOf(
mockk<ProxyWebSocketMessage>(),
mockk<ProxyWebSocketMessage>()
)

every { api.proxy() } returns proxy
every { proxy.webSocketHistory() } returns wsHistory

runBlocking {
val result = client.callTool(
"get_proxy_websocket_history_count", mapOf<String, Any>()
)

delay(100)
result.expectTextContent("2")
}
}

@Test
fun `get proxy websocket history count with regex should return filtered count`() {
val proxy = mockk<Proxy>()
val fullHistory = listOf(
mockk<ProxyWebSocketMessage>(),
mockk<ProxyWebSocketMessage>(),
mockk<ProxyWebSocketMessage>()
)
val filteredHistory = listOf(
mockk<ProxyWebSocketMessage>()
)

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<Collaborator>()
Expand Down