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
4 changes: 3 additions & 1 deletion src/main/kotlin/net/portswigger/mcp/ExtensionBase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import burp.api.montoya.MontoyaApi
import net.portswigger.mcp.config.ConfigUi
import net.portswigger.mcp.config.McpConfig
import net.portswigger.mcp.providers.ClaudeDesktopProvider
import net.portswigger.mcp.providers.GitHubCopilotCliProvider
import net.portswigger.mcp.providers.ManualProxyInstallerProvider
import net.portswigger.mcp.providers.ProxyJarManager

Expand All @@ -22,6 +23,7 @@ class ExtensionBase : BurpExtension {
val configUi = ConfigUi(
config = config, providers = listOf(
ClaudeDesktopProvider(api.logging(), proxyJarManager),
GitHubCopilotCliProvider(api.logging(), proxyJarManager),
ManualProxyInstallerProvider(api.logging(), proxyJarManager),
)
)
Expand Down Expand Up @@ -54,4 +56,4 @@ class ExtensionBase : BurpExtension {
}
}
}
}
}
64 changes: 63 additions & 1 deletion src/main/kotlin/net/portswigger/mcp/providers/Provider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import javax.swing.JFileChooser
import kotlin.io.path.createDirectories
import kotlin.io.path.exists
import kotlin.io.path.readText
import kotlin.io.path.writeText
Expand All @@ -19,6 +20,67 @@ interface Provider {
fun install(config: McpConfig): String?
}

class GitHubCopilotCliProvider(
private val logging: Logging,
private val proxyJarManager: ProxyJarManager,
private val userHome: Path = Path.of(System.getProperty("user.home"))
) : Provider {

private val configFileName = "mcp-config.json"
private val serverName = "burp"

override val name = "GitHub Copilot CLI"
override val installButtonText = "Install to $name"
override val confirmationText =
"Install to $name?\nThis will create or update $name's MCP configuration ($configFileName)."

override fun install(config: McpConfig): String {
val proxyJarFile = proxyJarManager.getProxyJar()
val path = configFilePath()
path.parent.createDirectories()

val content: MutableMap<String, JsonElement> = if (path.exists()) {
Json.parseToJsonElement(path.readText()).jsonObject.toMutableMap<String, JsonElement>()
} else {
mutableMapOf("mcpServers" to buildJsonObject {})
}

val burpServerConfig = buildJsonObject {
put("type", JsonPrimitive("local"))
put("command", JsonPrimitive("java"))
put("args", buildJsonArray {
add(JsonPrimitive("-jar"))
add(JsonPrimitive(proxyJarFile.toString()))
add(JsonPrimitive("--sse-url"))
add(JsonPrimitive("http://${config.host}:${config.port}"))
})
put("tools", buildJsonArray {
add(JsonPrimitive("*"))
})
}

val mcpServers = mutableMapOf<String, JsonElement>().apply {
content["mcpServers"]?.jsonObject?.let { putAll(it) }
}
mcpServers[serverName] = burpServerConfig
content["mcpServers"] = JsonObject(mcpServers)

val json = Json {
prettyPrint = true
encodeDefaults = true
}
path.writeText(json.encodeToString(JsonObject.serializer(), JsonObject(content)))

logging.logToOutput("Installed Burp MCP Server to GitHub Copilot CLI config")

return "Installation successful. Please restart $name if it is currently running."
}

private fun configFilePath(): Path {
return userHome.resolve(".copilot").resolve(configFileName)
}
}

class ClaudeDesktopProvider(private val logging: Logging, private val proxyJarManager: ProxyJarManager) : Provider {

private val claudeConfigFileName = "claude_desktop_config.json"
Expand Down Expand Up @@ -146,4 +208,4 @@ class ManualProxyInstallerProvider(private val logging: Logging, private val pro

return "Extracted proxy jar to $destinationFile"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package net.portswigger.mcp.providers

import burp.api.montoya.logging.Logging
import burp.api.montoya.persistence.PersistedObject
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import net.portswigger.mcp.config.McpConfig
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import java.nio.file.Path

class GitHubCopilotCliProviderTest {

@TempDir
lateinit var tempDir: Path

private lateinit var logging: Logging
private lateinit var config: McpConfig

@BeforeEach
fun setup() {
val storage = mutableMapOf<String, Any>()

val persistedObject = mockk<PersistedObject>().apply {
every { getBoolean(any()) } answers {
val key = firstArg<String>()
storage[key] as? Boolean ?: when (key) {
"enabled" -> true
else -> false
}
}
every { getString(any()) } answers {
val key = firstArg<String>()
storage[key] as? String ?: when (key) {
"host" -> "127.0.0.1"
else -> ""
}
}
every { getInteger(any()) } answers {
val key = firstArg<String>()
storage[key] as? Int ?: when (key) {
"port" -> 9876
else -> 0
}
}
every { setBoolean(any(), any()) } answers {
storage[firstArg()] = secondArg<Boolean>()
}
every { setString(any(), any()) } answers {
storage[firstArg()] = secondArg<String>()
}
every { setInteger(any(), any()) } answers {
storage[firstArg()] = secondArg<Int>()
}
}

logging = mockk<Logging>().apply {
every { logToOutput(any<String>()) } returns Unit
every { logToError(any<String>()) } returns Unit
}

config = McpConfig(persistedObject, logging)
}

@Test
fun `install should create Copilot config when missing`() {
val proxyJarManager = mockk<ProxyJarManager>().apply {
every { getProxyJar() } returns Path.of("/tmp/mcp-proxy-all.jar")
}
val provider = GitHubCopilotCliProvider(logging, proxyJarManager, tempDir)

val result = provider.install(config)

val configPath = tempDir.resolve(".copilot").resolve("mcp-config.json")
assertTrue(configPath.toFile().exists())
assertEquals("Installation successful. Please restart GitHub Copilot CLI if it is currently running.", result)

val json = Json.parseToJsonElement(configPath.toFile().readText()).jsonObject
val burp = json["mcpServers"]!!.jsonObject["burp"]!!.jsonObject
assertEquals("local", burp["type"]!!.jsonPrimitive.content)
assertEquals("java", burp["command"]!!.jsonPrimitive.content)
assertEquals("-jar", burp["args"]!!.jsonArray[0].jsonPrimitive.content)
assertEquals("/tmp/mcp-proxy-all.jar", burp["args"]!!.jsonArray[1].jsonPrimitive.content)
assertEquals("http://127.0.0.1:9876", burp["args"]!!.jsonArray[3].jsonPrimitive.content)
assertEquals("*", burp["tools"]!!.jsonArray[0].jsonPrimitive.content)
verify { logging.logToOutput("Installed Burp MCP Server to GitHub Copilot CLI config") }
}

@Test
fun `install should preserve existing config and replace burp entry`() {
val configPath = tempDir.resolve(".copilot").resolve("mcp-config.json")
configPath.parent.toFile().mkdirs()
configPath.toFile().writeText(
"""
{
"mcpServers": {
"other": {
"type": "local",
"command": "uvx",
"args": ["context7"]
},
"burp": {
"type": "remote",
"url": "http://old.example"
}
}
}
""".trimIndent()
)

val proxyJarManager = mockk<ProxyJarManager>().apply {
every { getProxyJar() } returns Path.of("/tmp/mcp-proxy-all.jar")
}
val provider = GitHubCopilotCliProvider(logging, proxyJarManager, tempDir)

provider.install(config)

val json = Json.parseToJsonElement(configPath.toFile().readText()).jsonObject
val servers = json["mcpServers"]!!.jsonObject
assertEquals("uvx", servers["other"]!!.jsonObject["command"]!!.jsonPrimitive.content)
assertEquals("local", servers["burp"]!!.jsonObject["type"]!!.jsonPrimitive.content)
assertEquals("java", servers["burp"]!!.jsonObject["command"]!!.jsonPrimitive.content)
assertEquals("/tmp/mcp-proxy-all.jar", servers["burp"]!!.jsonObject["args"]!!.jsonArray[1].jsonPrimitive.content)
}
}