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
6 changes: 6 additions & 0 deletions src/main/kotlin/net/portswigger/mcp/schema/serialization.kt
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ fun burp.api.montoya.http.message.HttpRequestResponse.toSerializableForm(): Http

fun ProxyHttpRequestResponse.toSerializableForm(): HttpRequestResponse {
return HttpRequestResponse(
// Burp's proxy history request number (the value shown in the Proxy > HTTP history UI),
// so MCP clients can reference a specific item by id instead of by list offset.
id = id(),
request = request()?.toString() ?: "<no request>",
response = response()?.toString() ?: "<no response>",
notes = annotations().notes()
Expand Down Expand Up @@ -114,6 +117,9 @@ enum class AuditIssueConfidence {

@Serializable
data class HttpRequestResponse(
// Proxy history request number. Null for items that don't originate from proxy
// history (e.g. issue request/responses), so it's omitted from their JSON output.
val id: Int? = null,
val request: String?,
val response: String?,
val notes: String?
Expand Down
21 changes: 21 additions & 0 deletions src/main/kotlin/net/portswigger/mcp/tools/Tools.kt
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,24 @@ fun Server.registerTools(api: MontoyaApi, config: McpConfig) {
.map { truncateIfNeeded(Json.encodeToString(it.toSerializableForm())) }
}

mcpTool<GetProxyHttpHistoryById>(
"Retrieves a single proxy HTTP history item by its Burp request id (the number shown in the " +
"Proxy > HTTP history UI, also returned as the 'id' field by get_proxy_http_history). " +
"Lets you reference a specific request directly instead of paging through history by offset."
) {
val allowed = runBlocking {
checkDataAccessOrDeny(DataAccessType.HTTP_HISTORY, config, api, "HTTP history")
}
if (!allowed) {
return@mcpTool "HTTP history access denied by Burp Suite"
}

val item = api.proxy().history().firstOrNull { it.id() == id }
?: return@mcpTool "No proxy HTTP history item found with id $id"

truncateIfNeeded(Json.encodeToString(item.toSerializableForm()))
}

mcpPaginatedTool<GetOrganizerItems>("Displays items within the Organizer tab") {
val allowed = runBlocking {
checkDataAccessOrDeny(DataAccessType.ORGANIZER, config, api, "Organizer")
Expand Down Expand Up @@ -519,6 +537,9 @@ data class GetProxyHttpHistory(override val count: Int, override val offset: Int
@Serializable
data class GetProxyHttpHistoryRegex(val regex: String, override val count: Int, override val offset: Int) : Paginated

@Serializable
data class GetProxyHttpHistoryById(val id: Int)

@Serializable
data class GetOrganizerItems(override val count: Int, override val offset: Int) : Paginated

Expand Down
52 changes: 51 additions & 1 deletion src/test/kotlin/net/portswigger/mcp/tools/ToolsKtTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -799,8 +799,58 @@ class ToolsKtTest {
assertEquals("Reached end of items", result3.expectTextContent())
}
}

@Test
fun `get proxy history by id returns the matching item`() {
val proxy = mockk<Proxy>()
val item1 = mockk<ProxyHttpRequestResponse>()
val item2 = mockk<ProxyHttpRequestResponse>()

every { item1.id() } returns 1
every { item2.id() } returns 2
every { api.proxy() } returns proxy
every { proxy.history() } returns listOf(item1, item2)

mockkStatic("net.portswigger.mcp.schema.SerializationKt")
every { item2.toSerializableForm() } returns HttpRequestResponse(
id = 2,
request = "GET /two HTTP/1.1",
response = "HTTP/1.1 200 OK",
notes = null
)

runBlocking {
val result = client.callTool(
"get_proxy_http_history_by_id", mapOf("id" to 2)
)

delay(100)
val text = result.expectTextContent()
assertTrue(text.contains("GET /two"), "Should return the item whose id matches")
assertTrue(text.contains("\"id\":2"), "Serialized item should expose its id")
}
}

@Test
fun `get proxy history by id returns not found for unknown id`() {
val proxy = mockk<Proxy>()
val item1 = mockk<ProxyHttpRequestResponse>()

every { item1.id() } returns 1
every { api.proxy() } returns proxy
every { proxy.history() } returns listOf(item1)

runBlocking {
val result = client.callTool(
"get_proxy_http_history_by_id", mapOf("id" to 99)
)

delay(100)
result.expectTextContent("No proxy HTTP history item found with id 99")
}
}
}

@Nested
inner class CollaboratorToolsTests {
private val collaborator = mockk<Collaborator>()
Expand Down