Skip to content

Commit 94ec6a5

Browse files
Add approval dialog box for MCP actions before they are executed
Signed-off-by: mydeveloperplanet <[email protected]>
1 parent 1e6b9a2 commit 94ec6a5

File tree

7 files changed

+234
-6
lines changed

7 files changed

+234
-6
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package com.devoxx.genie.service.mcp;
2+
3+
import com.intellij.openapi.project.Project;
4+
5+
import org.jetbrains.annotations.NotNull;
6+
7+
import dev.langchain4j.agent.tool.ToolSpecification;
8+
import dev.langchain4j.service.tool.ToolExecutor;
9+
import dev.langchain4j.service.tool.ToolProvider;
10+
import dev.langchain4j.service.tool.ToolProviderRequest;
11+
import dev.langchain4j.service.tool.ToolProviderResult;
12+
13+
public class ApprovalRequiredToolProvider implements ToolProvider {
14+
15+
private final ToolProvider delegate;
16+
private final Project project;
17+
18+
public ApprovalRequiredToolProvider(ToolProvider delegate, Project project) {
19+
this.delegate = delegate;
20+
this.project = project;
21+
}
22+
23+
@Override
24+
public ToolProviderResult provideTools(@NotNull ToolProviderRequest request) {
25+
ToolProviderResult delegateResult = delegate.provideTools(request);
26+
27+
ToolProviderResult.Builder builder = ToolProviderResult.builder();
28+
29+
for (var entry : delegateResult.tools().entrySet()) {
30+
ToolSpecification spec = entry.getKey();
31+
ToolExecutor originalExecutor = entry.getValue();
32+
33+
// Wrap the original executor
34+
ToolExecutor approvalExecutor = (toolExecutionRequest, memoryId) -> {
35+
boolean approved = MCPApprovalService.requestApproval(
36+
project,
37+
toolExecutionRequest.name(),
38+
toolExecutionRequest.arguments()
39+
);
40+
if (approved) {
41+
MCPService.logDebug("MCP tool execution approved: " + toolExecutionRequest.name());
42+
return originalExecutor.execute(toolExecutionRequest, memoryId);
43+
} else {
44+
MCPService.logDebug("MCP tool execution denied: " + toolExecutionRequest.name());
45+
return "Tool execution was denied by the user.";
46+
}
47+
};
48+
49+
builder.add(spec, approvalExecutor);
50+
}
51+
52+
return builder.build();
53+
}
54+
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package com.devoxx.genie.service.mcp;
2+
3+
import java.awt.BorderLayout;
4+
import java.awt.Dimension;
5+
import java.awt.GridBagConstraints;
6+
import java.awt.GridBagLayout;
7+
import java.util.concurrent.CompletableFuture;
8+
import java.util.concurrent.ExecutionException;
9+
import java.util.concurrent.TimeUnit;
10+
import java.util.concurrent.TimeoutException;
11+
12+
import javax.swing.Action;
13+
import javax.swing.JComponent;
14+
import javax.swing.JPanel;
15+
import javax.swing.JTextArea;
16+
17+
import com.devoxx.genie.ui.settings.DevoxxGenieStateService;
18+
import com.devoxx.genie.ui.util.NotificationUtil;
19+
import com.intellij.openapi.application.ApplicationManager;
20+
import com.intellij.openapi.project.Project;
21+
import com.intellij.openapi.ui.DialogWrapper;
22+
import com.intellij.openapi.ui.Messages;
23+
import com.intellij.ui.JBColor;
24+
import com.intellij.ui.components.JBLabel;
25+
import com.intellij.ui.components.JBScrollPane;
26+
import com.intellij.util.ui.JBUI;
27+
28+
import lombok.extern.slf4j.Slf4j;
29+
30+
import org.jetbrains.annotations.NotNull;
31+
import org.jetbrains.annotations.Nullable;
32+
33+
/**
34+
* Service for handling MCP function call approvals
35+
*/
36+
@Slf4j
37+
public class MCPApprovalService {
38+
39+
private static final int APPROVAL_TIMEOUT_SECONDS = 30;
40+
41+
/**
42+
* Request user approval for an MCP tool execution
43+
*
44+
* @param project The current project
45+
* @param toolName The name of the tool being called
46+
* @param arguments The arguments being passed to the tool
47+
* @return true if approved, false if denied or timed out
48+
*/
49+
public static boolean requestApproval(@Nullable Project project, @NotNull String toolName, @NotNull String arguments) {
50+
// Skip approval if running in headless mode or if approval is not required
51+
if (ApplicationManager.getApplication().isHeadlessEnvironment() ||
52+
!DevoxxGenieStateService.getInstance().getMcpApprovalRequired()) {
53+
return true;
54+
}
55+
56+
CompletableFuture<Boolean> approvalFuture = new CompletableFuture<>();
57+
58+
ApplicationManager.getApplication().invokeLater(() -> {
59+
MCPApprovalDialog dialog = new MCPApprovalDialog(project, toolName, arguments);
60+
boolean approved = dialog.showAndGet();
61+
approvalFuture.complete(approved);
62+
});
63+
64+
try {
65+
// Wait for user response with timeout
66+
return approvalFuture.get(APPROVAL_TIMEOUT_SECONDS, TimeUnit.SECONDS);
67+
} catch (InterruptedException | ExecutionException | TimeoutException e) {
68+
log.warn("MCP approval request timed out or was interrupted", e);
69+
NotificationUtil.sendNotification(project, "MCP tool execution was cancelled due to timeout");
70+
return false;
71+
}
72+
}
73+
74+
/**
75+
* Dialog for requesting MCP tool execution approval
76+
*/
77+
private static class MCPApprovalDialog extends DialogWrapper {
78+
private final String toolName;
79+
private final String arguments;
80+
81+
protected MCPApprovalDialog(@Nullable Project project, @NotNull String toolName, @NotNull String arguments) {
82+
super(project, false);
83+
this.toolName = toolName;
84+
this.arguments = arguments;
85+
setTitle("Approve MCP Tool Execution");
86+
setOKButtonText("Approve");
87+
setCancelButtonText("Deny");
88+
init();
89+
}
90+
91+
@Override
92+
protected @Nullable JComponent createCenterPanel() {
93+
JPanel panel = new JPanel(new BorderLayout());
94+
panel.setBorder(JBUI.Borders.empty(10));
95+
panel.setPreferredSize(new Dimension(500, 300));
96+
97+
// Create warning icon and message
98+
JPanel headerPanel = new JPanel(new BorderLayout());
99+
JBLabel iconLabel = new JBLabel(Messages.getWarningIcon());
100+
headerPanel.add(iconLabel, BorderLayout.WEST);
101+
102+
JBLabel messageLabel = new JBLabel("<html><b>The AI assistant wants to execute the following MCP tool:</b></html>");
103+
headerPanel.add(messageLabel, BorderLayout.CENTER);
104+
panel.add(headerPanel, BorderLayout.NORTH);
105+
106+
// Create tool info panel
107+
JPanel infoPanel = new JPanel(new GridBagLayout());
108+
GridBagConstraints c = new GridBagConstraints();
109+
c.fill = GridBagConstraints.HORIZONTAL;
110+
c.insets = JBUI.insets(5);
111+
112+
// Tool name
113+
c.gridx = 0;
114+
c.gridy = 0;
115+
c.weightx = 0.2;
116+
infoPanel.add(new JBLabel("<html><b>Tool:</b></html>"), c);
117+
118+
c.gridx = 1;
119+
c.weightx = 0.8;
120+
infoPanel.add(new JBLabel(toolName), c);
121+
122+
// Arguments
123+
c.gridx = 0;
124+
c.gridy = 1;
125+
c.weightx = 0.2;
126+
c.anchor = GridBagConstraints.NORTHWEST;
127+
infoPanel.add(new JBLabel("<html><b>Arguments:</b></html>"), c);
128+
129+
c.gridx = 1;
130+
c.weightx = 0.8;
131+
JTextArea argumentsArea = new JTextArea(arguments);
132+
argumentsArea.setEditable(false);
133+
argumentsArea.setLineWrap(true);
134+
argumentsArea.setWrapStyleWord(true);
135+
JBScrollPane scrollPane = new JBScrollPane(argumentsArea);
136+
scrollPane.setPreferredSize(new Dimension(350, 150));
137+
infoPanel.add(scrollPane, c);
138+
139+
panel.add(infoPanel, BorderLayout.CENTER);
140+
141+
// Add warning message
142+
JBLabel warningLabel = new JBLabel("<html><i>Warning: Only approve if you trust this tool execution.</i></html>");
143+
warningLabel.setForeground(JBColor.RED);
144+
panel.add(warningLabel, BorderLayout.SOUTH);
145+
146+
return panel;
147+
}
148+
149+
@Override
150+
protected Action @NotNull [] createActions() {
151+
return new Action[]{getOKAction(), getCancelAction()};
152+
}
153+
154+
}
155+
}

src/main/java/com/devoxx/genie/service/mcp/MCPExecutionService.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import com.devoxx.genie.ui.settings.DevoxxGenieStateService;
55
import com.intellij.openapi.Disposable;
66
import com.intellij.openapi.application.ApplicationManager;
7+
import com.intellij.openapi.project.Project;
8+
79
import dev.langchain4j.mcp.McpToolProvider;
810
import dev.langchain4j.mcp.client.DefaultMcpClient;
911
import dev.langchain4j.mcp.client.McpClient;
@@ -76,10 +78,11 @@ public void dispose() {
7678

7779
/**
7880
* Creates tool providers for all configured MCP servers
79-
*
81+
*
82+
* @param project Holds the project information
8083
* @return A ToolProvider that includes all enabled MCP tools, or null if MCP is disabled or no servers are configured
8184
*/
82-
public ToolProvider createMCPToolProvider() {
85+
public ToolProvider createMCPToolProvider(Project project) {
8386
log.debug("Creating MCP Tool Provider");
8487

8588
// Get all configured MCP servers
@@ -107,9 +110,13 @@ public ToolProvider createMCPToolProvider() {
107110
}
108111

109112
MCPService.logDebug("Creating MCP Tool Provider with " + mcpClients.size() + " clients");
110-
return McpToolProvider.builder()
113+
// Create the original MCP tool provider
114+
ToolProvider originalProvider = McpToolProvider.builder()
111115
.mcpClients(mcpClients)
112116
.build();
117+
118+
// Wrap it with the custom approval-requiring provider
119+
return new ApprovalRequiredToolProvider(originalProvider, project);
113120
}
114121

115122
/**

src/main/java/com/devoxx/genie/service/prompt/response/nonstreaming/NonStreamingPromptExecutionService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ public void cancelExecutingQuery() {
155155
MCPService.logDebug("MCP is enabled and we have active tools. Creating MCP tool provider");
156156

157157
// Use project-specific tool provider with filesystem access
158-
ToolProvider mcpToolProvider = MCPExecutionService.getInstance().createMCPToolProvider();
158+
ToolProvider mcpToolProvider = MCPExecutionService.getInstance().createMCPToolProvider(project);
159159

160160
if (mcpToolProvider != null) {
161161
MCPService.logDebug("Successfully created MCP tool provider with filesystem access");

src/main/java/com/devoxx/genie/service/prompt/strategy/StreamingPromptStrategy.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ protected void executeStrategySpecific(
159159

160160
Assistant assistant;
161161

162-
ToolProvider mcpToolProvider = MCPExecutionService.getInstance().createMCPToolProvider();
162+
ToolProvider mcpToolProvider = MCPExecutionService.getInstance().createMCPToolProvider(project);
163163
if (mcpToolProvider != null) {
164164
MCPService.logDebug("Successfully created MCP tool provider with filesystem access");
165165

src/main/java/com/devoxx/genie/ui/settings/DevoxxGenieStateService.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,9 @@ public static DevoxxGenieStateService getInstance() {
200200
@Setter
201201
@Getter
202202
private Boolean mcpDebugLogsEnabled = false;
203+
@Getter
204+
@Setter
205+
private Boolean mcpApprovalRequired = true;
203206

204207
// Appearance settings
205208
private Double lineHeight = 1.6; // Default line height multiplier

src/main/java/com/devoxx/genie/ui/settings/mcp/MCPSettingsComponent.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public class MCPSettingsComponent extends AbstractSettingsComponent {
3535
private boolean isModified = false;
3636
private final JCheckBox enableMcpCheckbox;
3737
private final JCheckBox enableDebugLogsCheckbox;
38+
private final JCheckBox enableApprovalRequiredCheckbox;
3839

3940
public MCPSettingsComponent() {
4041

@@ -47,6 +48,9 @@ public MCPSettingsComponent() {
4748

4849
enableDebugLogsCheckbox = new JCheckBox("Enable MCP Logging");
4950
enableDebugLogsCheckbox.addActionListener(e -> isModified = true);
51+
52+
enableApprovalRequiredCheckbox = new JCheckBox("Enable Approval Required");
53+
enableApprovalRequiredCheckbox.addActionListener(e -> isModified = true);
5054

5155
setupTable();
5256

@@ -67,6 +71,7 @@ public MCPSettingsComponent() {
6771
JPanel checkboxPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
6872
checkboxPanel.add(enableMcpCheckbox);
6973
checkboxPanel.add(enableDebugLogsCheckbox);
74+
checkboxPanel.add(enableApprovalRequiredCheckbox);
7075

7176
// Create the top panel that combines both
7277
JPanel topPanel = new JPanel(new BorderLayout());
@@ -87,6 +92,7 @@ public MCPSettingsComponent() {
8792
DevoxxGenieStateService stateService = DevoxxGenieStateService.getInstance();
8893
enableMcpCheckbox.setSelected(stateService.getMcpEnabled());
8994
enableDebugLogsCheckbox.setSelected(stateService.getMcpDebugLogsEnabled());
95+
enableApprovalRequiredCheckbox.setSelected(stateService.getMcpApprovalRequired());
9096
// Reset modified flag if needed, as initial load shouldn't count as modification
9197
isModified = false;
9298
});
@@ -215,6 +221,7 @@ public void apply() {
215221
// Save checkbox settings
216222
stateService.setMcpEnabled(enableMcpCheckbox.isSelected());
217223
stateService.setMcpDebugLogsEnabled(enableDebugLogsCheckbox.isSelected());
224+
stateService.setMcpApprovalRequired(enableApprovalRequiredCheckbox.isSelected());
218225

219226
// Refresh the tool window visibility if MCP enabled state changed
220227
if (oldMcpEnabled != enableMcpCheckbox.isSelected()) {
@@ -251,7 +258,8 @@ public boolean isModified() {
251258
DevoxxGenieStateService stateService = DevoxxGenieStateService.getInstance();
252259
return isModified ||
253260
enableMcpCheckbox.isSelected() != stateService.getMcpEnabled() ||
254-
enableDebugLogsCheckbox.isSelected() != stateService.getMcpDebugLogsEnabled();
261+
enableDebugLogsCheckbox.isSelected() != stateService.getMcpDebugLogsEnabled() ||
262+
enableApprovalRequiredCheckbox.isSelected() != stateService.getMcpApprovalRequired();
255263
}
256264

257265
/**
@@ -429,6 +437,7 @@ public void reset() {
429437
loadCurrentSettings();
430438
enableMcpCheckbox.setSelected(stateService.getMcpEnabled());
431439
enableDebugLogsCheckbox.setSelected(stateService.getMcpDebugLogsEnabled());
440+
enableApprovalRequiredCheckbox.setSelected(stateService.getMcpApprovalRequired());
432441
isModified = false;
433442

434443
// Find and update the Open MCP Log Panel button if it exists

0 commit comments

Comments
 (0)