Skip to content

Commit 4e12422

Browse files
Copilotphrocker
andauthored
Add UI for agent and MCP server management with deployment visualization (#63)
* Initial plan * Update dashboard UI with agent deployments and MCP server management Co-authored-by: phrocker <[email protected]> * Add enhanced Launch Agent modal with MCP server support and enable/disable toggles Co-authored-by: phrocker <[email protected]> * Remove TODOs, add MCP proxy controller, and implement GitHub token listing - Created MCPProxyController to proxy MCP API calls from frontend to integration-proxy service - Added /api/v1/integrations/github/list endpoint to list GitHub integration tokens - Removed all TODO comments and implemented proper functionality - Updated enclave-data.js to remove TODOs with appropriate messages - Updated add_agent.html to load GitHub tokens from API endpoint - All MCP endpoints now properly route through integration-proxy service Co-authored-by: phrocker <[email protected]> * Fix Authorization header and CSRF token issues - Removed required Authorization header from MCPProxyController methods - Extract JWT token from Spring Security context instead of requiring it from client - Added CSRF token to all MCP POST/DELETE requests in JavaScript - JavaScript now sends X-CSRF-TOKEN header instead of Authorization header - Fixes 302 redirect and missing Authorization header errors Co-authored-by: phrocker <[email protected]> * Use service principal authentication for MCP proxy calls - MCPProxyController now uses KeycloakService.getKeycloakToken() for service-to-service auth - Removed user token forwarding approach that caused 401 errors - Uses API's service principal JWT token instead of user's token - Follows the same pattern as AgentClientService for authenticated calls to other services - All MCP operations (launch, status, delete, list) now authenticate correctly Co-authored-by: phrocker <[email protected]> * Add Kubernetes RBAC for integration-proxy pod creation - Created ServiceAccount for integration-proxy - Created Role with pod and service creation permissions - Created RoleBinding to bind role to service account - Updated integration-proxy deployment to use the service account - Fixes 403 Forbidden error when creating MCP server pods - Matches RBAC setup pattern used by agent-launcher service Co-authored-by: phrocker <[email protected]> * fixup * Fix GitHub integration link in Launch Agent modal - Changed link from /sso/v1/integrations/github/add to /sso/v1/integrations/github - Fixes NoResourceFoundException when clicking "Add New GitHub Token" button - Links to existing GitHub integration page in IntegrationController Co-authored-by: phrocker <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: phrocker <[email protected]> Co-authored-by: Marc Parisi <[email protected]>
1 parent dda6656 commit 4e12422

File tree

13 files changed

+961
-109
lines changed

13 files changed

+961
-109
lines changed

api/src/main/java/io/sentrius/sso/controllers/api/IntegrationApiController.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import lombok.extern.slf4j.Slf4j;
2222
import org.springframework.http.ResponseEntity;
2323
import org.springframework.stereotype.Controller;
24+
import org.springframework.web.bind.annotation.GetMapping;
2425
import org.springframework.web.bind.annotation.PostMapping;
2526
import org.springframework.web.bind.annotation.RequestBody;
2627
import org.springframework.web.bind.annotation.RequestMapping;
@@ -137,4 +138,17 @@ public ResponseEntity<String> deleteIntegration(HttpServletRequest request,
137138
return ResponseEntity.ok("OK");
138139
}
139140

141+
@GetMapping("/github/list")
142+
@Endpoint(description = "List all GitHub integration tokens")
143+
public ResponseEntity<?> listGitHubIntegrations(HttpServletRequest request,
144+
HttpServletResponse response) {
145+
try {
146+
var tokens = integrationService.findByConnectionType("github");
147+
return ResponseEntity.ok(tokens);
148+
} catch (Exception e) {
149+
log.error("Error listing GitHub integrations", e);
150+
return ResponseEntity.status(500).body(Map.of("error", "Failed to list GitHub integrations"));
151+
}
152+
}
153+
140154
}
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
package io.sentrius.sso.controllers.api;
2+
3+
import io.sentrius.sso.core.annotations.LimitAccess;
4+
import io.sentrius.sso.core.exceptions.ZtatException;
5+
import io.sentrius.sso.core.model.security.enums.ApplicationAccessEnum;
6+
import io.sentrius.sso.core.services.security.KeycloakService;
7+
import jakarta.servlet.http.HttpServletRequest;
8+
import jakarta.servlet.http.HttpServletResponse;
9+
import lombok.extern.slf4j.Slf4j;
10+
import org.springframework.beans.factory.annotation.Value;
11+
import org.springframework.http.*;
12+
import org.springframework.web.bind.annotation.*;
13+
import org.springframework.web.client.HttpClientErrorException;
14+
import org.springframework.web.client.RestTemplate;
15+
16+
import java.net.URI;
17+
import java.util.Map;
18+
19+
/**
20+
* Proxy controller to forward MCP-related API calls from the frontend
21+
* to the integration-proxy service using service principal authentication
22+
*/
23+
@RestController
24+
@RequestMapping("/api/v1")
25+
@Slf4j
26+
public class MCPProxyController {
27+
28+
@Value("${integration.proxy.url:http://sentrius-integrationproxy:8080}")
29+
private String integrationProxyUrl;
30+
31+
private final KeycloakService keycloakService;
32+
private final RestTemplate restTemplate = new RestTemplate();
33+
34+
public MCPProxyController(KeycloakService keycloakService) {
35+
this.keycloakService = keycloakService;
36+
}
37+
38+
/**
39+
* Proxy GitHub MCP launch requests to integration-proxy
40+
*/
41+
@PostMapping("/github/mcp/launch")
42+
@LimitAccess(applicationAccess = {ApplicationAccessEnum.CAN_LOG_IN})
43+
public ResponseEntity<?> launchGitHubMCP(
44+
@RequestParam String tokenId,
45+
HttpServletRequest request,
46+
HttpServletResponse response
47+
) {
48+
try {
49+
String url = integrationProxyUrl + "/api/v1/github/mcp/launch?tokenId=" + tokenId;
50+
return forwardRequest(url, HttpMethod.POST, null);
51+
} catch (Exception e) {
52+
log.error("Error launching GitHub MCP server", e);
53+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
54+
.body(Map.of("error", "Failed to launch GitHub MCP server: " + e.getMessage()));
55+
}
56+
}
57+
58+
/**
59+
* Proxy GitHub MCP status requests to integration-proxy
60+
*/
61+
@GetMapping("/github/mcp/status")
62+
@LimitAccess(applicationAccess = {ApplicationAccessEnum.CAN_LOG_IN})
63+
public ResponseEntity<?> getGitHubMCPStatus(
64+
@RequestParam String tokenId,
65+
HttpServletRequest request,
66+
HttpServletResponse response
67+
) {
68+
try {
69+
String url = integrationProxyUrl + "/api/v1/github/mcp/status?tokenId=" + tokenId;
70+
return forwardRequest(url, HttpMethod.GET, null);
71+
} catch (Exception e) {
72+
log.error("Error getting GitHub MCP server status", e);
73+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
74+
.body(Map.of("error", "Failed to get GitHub MCP server status: " + e.getMessage()));
75+
}
76+
}
77+
78+
/**
79+
* Proxy GitHub MCP delete requests to integration-proxy
80+
*/
81+
@DeleteMapping("/github/mcp/delete")
82+
@LimitAccess(applicationAccess = {ApplicationAccessEnum.CAN_LOG_IN})
83+
public ResponseEntity<?> deleteGitHubMCP(
84+
@RequestParam String tokenId,
85+
HttpServletRequest request,
86+
HttpServletResponse response
87+
) {
88+
try {
89+
String url = integrationProxyUrl + "/api/v1/github/mcp/delete?tokenId=" + tokenId;
90+
return forwardRequest(url, HttpMethod.DELETE, null);
91+
} catch (Exception e) {
92+
log.error("Error deleting GitHub MCP server", e);
93+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
94+
.body(Map.of("error", "Failed to delete GitHub MCP server: " + e.getMessage()));
95+
}
96+
}
97+
98+
/**
99+
* Proxy GitHub MCP list requests to integration-proxy
100+
*/
101+
@GetMapping("/github/mcp/list")
102+
@LimitAccess(applicationAccess = {ApplicationAccessEnum.CAN_LOG_IN})
103+
public ResponseEntity<?> listGitHubMCP(
104+
HttpServletRequest request,
105+
HttpServletResponse response
106+
) {
107+
try {
108+
String url = integrationProxyUrl + "/api/v1/github/mcp/list";
109+
return forwardRequest(url, HttpMethod.GET, null);
110+
} catch (Exception e) {
111+
log.error("Error listing GitHub MCP servers", e);
112+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
113+
.body(Map.of("error", "Failed to list GitHub MCP servers: " + e.getMessage()));
114+
}
115+
}
116+
117+
/**
118+
* Proxy Coding MCP launch requests to integration-proxy
119+
*/
120+
@PostMapping("/coding/mcp/launch")
121+
@LimitAccess(applicationAccess = {ApplicationAccessEnum.CAN_LOG_IN})
122+
public ResponseEntity<?> launchCodingMCP(
123+
@RequestParam(required = false, defaultValue = "default") String instanceId,
124+
@RequestParam(required = false) String githubTokenId,
125+
HttpServletRequest request,
126+
HttpServletResponse response
127+
) {
128+
try {
129+
StringBuilder url = new StringBuilder(integrationProxyUrl + "/api/v1/coding/mcp/launch?instanceId=" + instanceId);
130+
if (githubTokenId != null && !githubTokenId.isEmpty()) {
131+
url.append("&githubTokenId=").append(githubTokenId);
132+
}
133+
return forwardRequest(url.toString(), HttpMethod.POST, null);
134+
} catch (Exception e) {
135+
log.error("Error launching Coding MCP server", e);
136+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
137+
.body(Map.of("error", "Failed to launch Coding MCP server: " + e.getMessage()));
138+
}
139+
}
140+
141+
/**
142+
* Proxy Coding MCP status requests to integration-proxy
143+
*/
144+
@GetMapping("/coding/mcp/status")
145+
@LimitAccess(applicationAccess = {ApplicationAccessEnum.CAN_LOG_IN})
146+
public ResponseEntity<?> getCodingMCPStatus(
147+
@RequestParam(required = false, defaultValue = "default") String instanceId,
148+
HttpServletRequest request,
149+
HttpServletResponse response
150+
) {
151+
try {
152+
String url = integrationProxyUrl + "/api/v1/coding/mcp/status?instanceId=" + instanceId;
153+
return forwardRequest(url, HttpMethod.GET, null);
154+
} catch (Exception e) {
155+
log.error("Error getting Coding MCP server status", e);
156+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
157+
.body(Map.of("error", "Failed to get Coding MCP server status: " + e.getMessage()));
158+
}
159+
}
160+
161+
/**
162+
* Proxy Coding MCP delete requests to integration-proxy
163+
*/
164+
@DeleteMapping("/coding/mcp/delete")
165+
@LimitAccess(applicationAccess = {ApplicationAccessEnum.CAN_LOG_IN})
166+
public ResponseEntity<?> deleteCodingMCP(
167+
@RequestParam(required = false, defaultValue = "default") String instanceId,
168+
HttpServletRequest request,
169+
HttpServletResponse response
170+
) {
171+
try {
172+
String url = integrationProxyUrl + "/api/v1/coding/mcp/delete?instanceId=" + instanceId;
173+
return forwardRequest(url, HttpMethod.DELETE, null);
174+
} catch (Exception e) {
175+
log.error("Error deleting Coding MCP server", e);
176+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
177+
.body(Map.of("error", "Failed to delete Coding MCP server: " + e.getMessage()));
178+
}
179+
}
180+
181+
/**
182+
* Proxy Coding MCP list requests to integration-proxy
183+
*/
184+
@GetMapping("/coding/mcp/list")
185+
@LimitAccess(applicationAccess = {ApplicationAccessEnum.CAN_LOG_IN})
186+
public ResponseEntity<?> listCodingMCP(
187+
HttpServletRequest request,
188+
HttpServletResponse response
189+
) {
190+
try {
191+
String url = integrationProxyUrl + "/api/v1/coding/mcp/list";
192+
return forwardRequest(url, HttpMethod.GET, null);
193+
} catch (Exception e) {
194+
log.error("Error listing Coding MCP servers", e);
195+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
196+
.body(Map.of("error", "Failed to list Coding MCP servers: " + e.getMessage()));
197+
}
198+
}
199+
200+
/**
201+
* Forward requests to integration-proxy using service principal authentication
202+
*/
203+
private ResponseEntity<?> forwardRequest(String url, HttpMethod method, Object body) {
204+
try {
205+
// Get service principal JWT token from Keycloak
206+
String keycloakJwt = keycloakService.getKeycloakToken();
207+
208+
HttpHeaders headers = new HttpHeaders();
209+
headers.setContentType(MediaType.APPLICATION_JSON);
210+
headers.setBearerAuth(keycloakJwt);
211+
212+
HttpEntity<?> entity = new HttpEntity<>(body, headers);
213+
214+
log.info("Forwarding {} request to integration-proxy: {} with service principal auth", method, url);
215+
ResponseEntity<String> httpResponse = restTemplate.exchange(url, method, entity, String.class);
216+
217+
return ResponseEntity.status(httpResponse.getStatusCode()).body(httpResponse.getBody());
218+
} catch (HttpClientErrorException e) {
219+
log.error("HTTP error forwarding request to integration-proxy: {} - {}", url, e.getMessage());
220+
return ResponseEntity.status(e.getStatusCode())
221+
.body(Map.of("error", "Integration proxy error: " + e.getResponseBodyAsString()));
222+
} catch (Exception e) {
223+
log.error("Error forwarding request to integration-proxy: {}", url, e);
224+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
225+
.body(Map.of("error", "Failed to communicate with integration-proxy: " + e.getMessage()));
226+
}
227+
}
228+
}

api/src/main/java/io/sentrius/sso/controllers/api/agents/AgentApiController.java

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import java.security.NoSuchAlgorithmException;
77
import java.sql.SQLException;
88
import java.time.LocalDateTime;
9+
import java.util.HashMap;
910
import java.util.HashSet;
1011
import java.util.List;
1112
import java.util.Map;
@@ -857,6 +858,60 @@ public ResponseEntity<AgentContextDTO> createContext(
857858
return ResponseEntity.ok(dto);
858859
}
859860

861+
@GetMapping("/stats")
862+
@LimitAccess(applicationAccess = {ApplicationAccessEnum.CAN_LOG_IN})
863+
public ResponseEntity<?> getAgentStats(HttpServletRequest request, HttpServletResponse response) throws ZtatException {
864+
try {
865+
var operatingUser = getOperatingUser(request, response);
866+
if (null == operatingUser) {
867+
log.warn("No operating user found");
868+
return ResponseEntity.status(HttpStatus.SC_UNAUTHORIZED).body("No operating user found");
869+
}
870+
871+
log.info("Received agent stats request from user: {}", operatingUser.getUsername());
872+
var agents = agentService.getAllAgents(true);
873+
874+
// Count agents by type and status
875+
Map<String, Long> stats = new HashMap<>();
876+
long runningCount = 0;
877+
long pendingCount = 0;
878+
long failedCount = 0;
879+
880+
for (AgentDTO agent : agents) {
881+
try {
882+
if (agent.getAgentName() != null && !agent.getAgentName().isEmpty()) {
883+
String podStatus = agentClientService.getAgentPodStatus(
884+
appConfig.getSentriusLauncherService(),
885+
agent.getAgentName()
886+
);
887+
888+
if (podStatus != null) {
889+
if (podStatus.equalsIgnoreCase("running")) {
890+
runningCount++;
891+
} else if (podStatus.equalsIgnoreCase("pending")) {
892+
pendingCount++;
893+
} else {
894+
failedCount++;
895+
}
896+
}
897+
}
898+
} catch (Exception ignored) {
899+
// Skip agents that fail to query
900+
}
901+
}
902+
903+
stats.put("Running", runningCount);
904+
stats.put("Pending", pendingCount);
905+
stats.put("Stopped", failedCount);
906+
907+
return ResponseEntity.ok(stats);
908+
909+
} catch (Exception e) {
910+
log.error("Error getting agent statistics", e);
911+
return ResponseEntity.status(HttpStatus.SC_INTERNAL_SERVER_ERROR)
912+
.body(Map.of("error", "Failed to get agent statistics"));
913+
}
914+
}
860915

861916

862917
}

0 commit comments

Comments
 (0)