1+ package io .sentrius .sso .controllers .api .mcp ;
2+
3+ import io .sentrius .sso .core .controllers .BaseController ;
4+ import io .sentrius .sso .core .annotations .LimitAccess ;
5+ import io .sentrius .sso .core .model .security .enums .ApplicationAccessEnum ;
6+ import io .sentrius .sso .core .services .UserService ;
7+ import io .sentrius .sso .core .config .SystemOptions ;
8+ import io .sentrius .sso .core .services .ErrorOutputService ;
9+ import io .sentrius .sso .core .services .security .KeycloakService ;
10+ import io .sentrius .sso .mcp .model .MCPRequest ;
11+ import io .sentrius .sso .mcp .model .MCPResponse ;
12+ import io .sentrius .sso .mcp .model .MCPError ;
13+ import io .sentrius .sso .mcp .service .MCPProxyService ;
14+
15+ import com .fasterxml .jackson .core .JsonProcessingException ;
16+ import com .fasterxml .jackson .databind .ObjectMapper ;
17+ import lombok .extern .slf4j .Slf4j ;
18+ import org .springframework .http .HttpStatus ;
19+ import org .springframework .http .ResponseEntity ;
20+ import org .springframework .web .bind .annotation .*;
21+
22+ import jakarta .servlet .http .HttpServletRequest ;
23+ import jakarta .servlet .http .HttpServletResponse ;
24+ import java .util .Map ;
25+
26+ /**
27+ * MCP (Model Context Protocol) Proxy Controller with Zero Trust Security
28+ *
29+ * Provides secure MCP endpoints with the same security controls as other Sentrius services:
30+ * - JWT authentication via Keycloak
31+ * - Zero Trust Access Token (ZTAT) validation
32+ * - Access control via @LimitAccess annotations
33+ * - Provenance tracking for audit trails
34+ */
35+ @ RestController
36+ @ RequestMapping ("/api/v1/mcp" )
37+ @ Slf4j
38+ public class MCPProxyController extends BaseController {
39+
40+ private final KeycloakService keycloakService ;
41+ private final MCPProxyService mcpProxyService ;
42+ private final ObjectMapper objectMapper ;
43+
44+ public MCPProxyController (
45+ UserService userService ,
46+ SystemOptions systemOptions ,
47+ ErrorOutputService errorOutputService ,
48+ KeycloakService keycloakService ,
49+ MCPProxyService mcpProxyService ,
50+ ObjectMapper objectMapper
51+ ) {
52+ super (userService , systemOptions , errorOutputService );
53+ this .keycloakService = keycloakService ;
54+ this .mcpProxyService = mcpProxyService ;
55+ this .objectMapper = objectMapper ;
56+ }
57+
58+ /**
59+ * Handle MCP requests via HTTP POST
60+ */
61+ @ PostMapping ("/" )
62+ @ LimitAccess (applicationAccess = {ApplicationAccessEnum .CAN_LOG_IN })
63+ public ResponseEntity <?> handleMCPRequest (
64+ @ RequestHeader ("Authorization" ) String token ,
65+ @ RequestHeader ("communication_id" ) String communicationId ,
66+ HttpServletRequest request ,
67+ HttpServletResponse response ,
68+ @ RequestBody String rawBody ) {
69+
70+ log .info ("Received MCP request with communication_id: {}" , communicationId );
71+
72+ String compactJwt = extractJwtToken (token );
73+
74+ // Validate JWT token
75+ if (!keycloakService .validateJwt (compactJwt )) {
76+ log .warn ("Invalid Keycloak token for MCP request" );
77+ return ResponseEntity .status (HttpStatus .UNAUTHORIZED )
78+ .body (createErrorResponse (null , MCPError .unauthorized ("Invalid Keycloak token" )));
79+ }
80+
81+ // Get operating user
82+ var operatingUser = getOperatingUser (request , response );
83+ if (operatingUser == null ) {
84+ log .warn ("No operating user found for MCP request" );
85+ return ResponseEntity .status (HttpStatus .UNAUTHORIZED )
86+ .body (createErrorResponse (null , MCPError .unauthorized ("No operating user found" )));
87+ }
88+
89+ try {
90+ // Parse MCP request
91+ MCPRequest mcpRequest = objectMapper .readValue (rawBody , MCPRequest .class );
92+
93+ // Validate MCP request structure
94+ if (mcpRequest .getMethod () == null || mcpRequest .getId () == null ) {
95+ return ResponseEntity .badRequest ()
96+ .body (createErrorResponse (mcpRequest .getId (), MCPError .invalidRequest ("Missing required fields" )));
97+ }
98+
99+ // Process the request through the service layer
100+ MCPResponse mcpResponse = mcpProxyService .processRequest (
101+ mcpRequest , compactJwt , communicationId , operatingUser .getUsername ()
102+ );
103+
104+ return ResponseEntity .ok (mcpResponse );
105+
106+ } catch (JsonProcessingException e ) {
107+ log .error ("Failed to parse MCP request" , e );
108+ return ResponseEntity .badRequest ()
109+ .body (createErrorResponse (null , MCPError .parseError ("Invalid JSON format" )));
110+ } catch (Exception e ) {
111+ log .error ("Unexpected error processing MCP request" , e );
112+ return ResponseEntity .status (HttpStatus .INTERNAL_SERVER_ERROR )
113+ .body (createErrorResponse (null , MCPError .internalError ("Internal server error" )));
114+ }
115+ }
116+
117+ /**
118+ * Handle MCP capability discovery
119+ */
120+ @ GetMapping ("/capabilities" )
121+ @ LimitAccess (applicationAccess = {ApplicationAccessEnum .CAN_LOG_IN })
122+ public ResponseEntity <?> getCapabilities (
123+ @ RequestHeader ("Authorization" ) String token ,
124+ HttpServletRequest request ,
125+ HttpServletResponse response ) {
126+
127+ String compactJwt = extractJwtToken (token );
128+
129+ if (!keycloakService .validateJwt (compactJwt )) {
130+ log .warn ("Invalid Keycloak token for MCP capabilities request" );
131+ return ResponseEntity .status (HttpStatus .UNAUTHORIZED )
132+ .body (MCPError .unauthorized ("Invalid Keycloak token" ));
133+ }
134+
135+ var operatingUser = getOperatingUser (request , response );
136+ if (operatingUser == null ) {
137+ return ResponseEntity .status (HttpStatus .UNAUTHORIZED )
138+ .body (MCPError .unauthorized ("No operating user found" ));
139+ }
140+
141+ try {
142+ // Create an initialize request to get capabilities
143+ MCPRequest initRequest = MCPRequest .create ("capabilities" , "initialize" , null );
144+ MCPResponse mcpResponse = mcpProxyService .processRequest (
145+ initRequest , compactJwt , "capabilities" , operatingUser .getUsername ()
146+ );
147+
148+ return ResponseEntity .ok (mcpResponse .getResult ());
149+
150+ } catch (Exception e ) {
151+ log .error ("Error getting MCP capabilities" , e );
152+ return ResponseEntity .status (HttpStatus .INTERNAL_SERVER_ERROR )
153+ .body (MCPError .internalError ("Failed to get capabilities" ));
154+ }
155+ }
156+
157+ /**
158+ * Health check endpoint for MCP proxy
159+ */
160+ @ GetMapping ("/health" )
161+ public ResponseEntity <?> health () {
162+ return ResponseEntity .ok (Map .of (
163+ "status" , "healthy" ,
164+ "service" , "mcp-proxy" ,
165+ "timestamp" , java .time .Instant .now ().toString ()
166+ ));
167+ }
168+
169+ /**
170+ * Extract JWT token from Authorization header
171+ */
172+ private String extractJwtToken (String authHeader ) {
173+ return authHeader != null && authHeader .startsWith ("Bearer " ) ?
174+ authHeader .substring (7 ) : authHeader ;
175+ }
176+
177+ /**
178+ * Create error response in MCP format
179+ */
180+ private MCPResponse createErrorResponse (String id , MCPError error ) {
181+ return MCPResponse .error (id , error );
182+ }
183+ }
0 commit comments