|
4 | 4 |
|
5 | 5 | package com.jfrog.jetbrains.actions |
6 | 6 |
|
7 | | -import com.intellij.notification.NotificationGroupManager |
8 | | -import com.intellij.notification.NotificationType |
9 | 7 | import com.intellij.openapi.actionSystem.AnAction |
10 | 8 | import com.intellij.openapi.actionSystem.AnActionEvent |
11 | 9 | import com.intellij.openapi.ide.CopyPasteManager |
| 10 | +import com.intellij.openapi.options.Configurable |
| 11 | +import com.intellij.openapi.options.ConfigurableWithId |
12 | 12 | import com.intellij.openapi.options.ShowSettingsUtil |
| 13 | +import com.intellij.openapi.ui.DialogWrapper |
| 14 | +import com.intellij.ui.components.JBScrollPane |
| 15 | +import com.intellij.ui.components.JBTextArea |
| 16 | +import com.intellij.util.ui.JBUI |
| 17 | +import java.awt.Dimension |
13 | 18 | import java.awt.datatransfer.StringSelection |
| 19 | +import java.util.function.Consumer |
| 20 | +import java.util.function.Predicate |
| 21 | +import javax.swing.JComponent |
| 22 | +import javax.swing.JLabel |
14 | 23 |
|
15 | 24 | // Fallback path (see CONTRIBUTING.md "Known open risk"): if the mcpTool |
16 | 25 | // extensions in plugin.xml turn out not to reach Junie's own tool-calling, |
17 | | -// this copies the JFrog MCP server JSON to the clipboard and opens Settings |
18 | | -// so the user can paste it into Settings | Tools | AI Assistant | MCP by |
19 | | -// hand - the same one-time manual step every other JFrog IDE integration |
20 | | -// already asks for around the platform URL. |
| 26 | +// this copies an MCP server entry to the clipboard and opens Settings so |
| 27 | +// the user can paste it into Settings | Tools | AI Assistant | Model |
| 28 | +// Context Protocol (MCP) by hand. That page (Configurable id "ml.llm.mcp", |
| 29 | +// confirmed by decompiling com.intellij.ml.llm.mcp.client.settings. |
| 30 | +// McpConfigurable.getId() in the bundled AI Assistant plugin's ml-llm.jar) |
| 31 | +// belongs to the AI Assistant/Junie plugin, not the separate |
| 32 | +// com.intellij.mcpServer plugin (Settings | Tools | MCP Server) that this |
| 33 | +// plugin's own mcpTool extensions register against - those are two |
| 34 | +// different, unrelated MCP integrations that happen to sit under the same |
| 35 | +// Tools settings group. |
| 36 | +// |
| 37 | +// We navigate with the (Project, Predicate, Consumer) overload rather than |
| 38 | +// the (Project, String) one, which matches by *display name*, not id |
| 39 | +// (confirmed by decompiling ShowSettingsUtilImpl.showSettingsDialog(Project, |
| 40 | +// String), which calls findPreselectedByDisplayName) - passing "ml.llm.mcp" |
| 41 | +// there never matched anything and silently fell back to the top of the |
| 42 | +// settings tree. The predicate overload throws IllegalStateException if |
| 43 | +// nothing matches instead of failing silently, so a missing AI Assistant |
| 44 | +// plugin is handled explicitly below rather than degrading to a no-op. |
| 45 | +// |
| 46 | +// JFrog's MCP server is remote (see github.com/jfrog/jfrog-mcp-server), |
| 47 | +// so its documented client config is a plain "url" entry - no local |
| 48 | +// binary. This settings page's server entries only support |
| 49 | +// command/args/env, though: confirmed by decompiling |
| 50 | +// mcp/client/settings/McpServerBean.class (no url field anywhere) and by |
| 51 | +// the literal string ml.llm.mcp.server.json.config.invalid = "Failed to |
| 52 | +// parse configuration" in the plugin's own McpBundle.properties. There is |
| 53 | +// no way to make this JSON paste-able on this page in this IDE build; it |
| 54 | +// is JFrog's real format, kept as-is for use in a client that does |
| 55 | +// support remote/url servers, and the dialog below says so explicitly |
| 56 | +// rather than sending the user looking for a paste option that isn't there. |
21 | 57 | class ConfigureJfrogMcpAction : AnAction() { |
22 | 58 | override fun actionPerformed(e: AnActionEvent) { |
| 59 | + val rawUrl = System.getenv("JFROG_PLATFORM_URL")?.trimEnd('/') |
| 60 | + val platformUrl = when { |
| 61 | + rawUrl == null -> "<JFROG_PLATFORM_URL>" |
| 62 | + rawUrl.startsWith("http://") || rawUrl.startsWith("https://") -> rawUrl |
| 63 | + else -> "https://$rawUrl" |
| 64 | + } |
23 | 65 | val json = """ |
24 | 66 | { |
25 | 67 | "mcpServers": { |
26 | 68 | "jfrog": { |
27 | | - "url": "https://${'$'}{JFROG_PLATFORM_URL}/mcp" |
| 69 | + "url": "$platformUrl/mcp" |
28 | 70 | } |
29 | 71 | } |
30 | 72 | } |
31 | 73 | """.trimIndent() |
32 | 74 |
|
33 | 75 | CopyPasteManager.getInstance().setContents(StringSelection(json)) |
34 | 76 |
|
35 | | - NotificationGroupManager.getInstance() |
36 | | - .getNotificationGroup("JFrog") |
37 | | - .createNotification( |
38 | | - "JFrog MCP server JSON copied to clipboard", |
39 | | - "Paste it into Settings | Tools | AI Assistant | MCP, replacing \${JFROG_PLATFORM_URL} with your JFrog platform URL.", |
40 | | - NotificationType.INFORMATION, |
41 | | - ) |
42 | | - .notify(e.project) |
| 77 | + val instructions = if (rawUrl == null) { |
| 78 | + "Set the JFROG_PLATFORM_URL environment variable to your JFrog platform URL and rerun this action, or replace <JFROG_PLATFORM_URL> yourself before pasting." |
| 79 | + } else { |
| 80 | + "The URL was filled in from your JFROG_PLATFORM_URL environment variable." |
| 81 | + } |
43 | 82 |
|
44 | | - e.project?.let { ShowSettingsUtil.getInstance().showSettingsDialog(it) } |
| 83 | + JfrogMcpConfigDialog(json, instructions).show() |
| 84 | + |
| 85 | + e.project?.let { project -> |
| 86 | + try { |
| 87 | + ShowSettingsUtil.getInstance().showSettingsDialog( |
| 88 | + project, |
| 89 | + Predicate<Configurable> { it is ConfigurableWithId && it.id == "ml.llm.mcp" }, |
| 90 | + Consumer<Configurable> {}, |
| 91 | + ) |
| 92 | + } catch (_: IllegalStateException) { |
| 93 | + // AI Assistant plugin not installed - the config is already |
| 94 | + // on the clipboard and explained in the dialog above. |
| 95 | + } |
| 96 | + } |
| 97 | + } |
| 98 | +} |
| 99 | + |
| 100 | +private class JfrogMcpConfigDialog( |
| 101 | + private val json: String, |
| 102 | + private val instructions: String, |
| 103 | +) : DialogWrapper(null, false) { |
| 104 | + init { |
| 105 | + title = "JFrog MCP Server Config Copied to Clipboard" |
| 106 | + setOKButtonText("Close") |
| 107 | + init() |
| 108 | + } |
| 109 | + |
| 110 | + override fun createCenterPanel(): JComponent { |
| 111 | + val message = JLabel( |
| 112 | + "<html>This is JFrog's remote MCP server config. Settings | Tools | AI Assistant | " + |
| 113 | + "Model Context Protocol (MCP) only accepts local command-based servers, so pasting " + |
| 114 | + "it there via + | As JSON will not launch anything - use this JSON in an MCP client " + |
| 115 | + "that supports remote/url servers instead.<br><br>$instructions</html>", |
| 116 | + ) |
| 117 | + val textArea = JBTextArea(json).apply { |
| 118 | + isEditable = false |
| 119 | + lineWrap = false |
| 120 | + } |
| 121 | + val scrollPane = JBScrollPane(textArea).apply { |
| 122 | + preferredSize = Dimension(520, 160) |
| 123 | + } |
| 124 | + |
| 125 | + val panel = com.intellij.util.ui.FormBuilder.createFormBuilder() |
| 126 | + .addComponent(message) |
| 127 | + .addComponentFillVertically(scrollPane, JBUI.scale(8)) |
| 128 | + .panel |
| 129 | + panel.preferredSize = Dimension(560, 320) |
| 130 | + return panel |
45 | 131 | } |
46 | 132 | } |
0 commit comments