-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathMcpController.java
More file actions
145 lines (133 loc) · 6.44 KB
/
Copy pathMcpController.java
File metadata and controls
145 lines (133 loc) · 6.44 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
package com.dbaagent.controller;
import com.dbaagent.model.ExplainPlanAnalysis;
import com.dbaagent.model.McpReadOnlyExplainRequest;
import com.dbaagent.model.McpReadOnlyQueryRequest;
import com.dbaagent.model.QueryExecutionOrigin;
import com.dbaagent.model.QueryRequest;
import com.dbaagent.model.QueryResult;
import com.dbaagent.service.ExplainPlanService;
import com.dbaagent.service.McpSqlGuardService;
import com.dbaagent.service.QueryExecutionContext;
import com.dbaagent.service.QueryExecutionPolicyException;
import com.dbaagent.service.QueryExecutorService;
import com.dbaagent.service.security.AccessControlService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
import java.util.Map;
/**
* @deprecated MCP/CLI clients now hit the canonical /api/connections/{id}/query
* and /api/explain/analyze endpoints, which run the same policy as the web SQL
* Editor (role-gated admin mutations, WHERE-clause guard, two-step confirm) and
* emit audit events via {@link com.dbaagent.service.SqlExecutionAuditService}.
*
* These read-only-locked endpoints are kept as a one-cycle alias for older MCP
* clients (@deepsql/mcp < 0.13.0) and will be removed in the 0.14.0 backend.
* Every call logs a deprecation warning so we can confirm zero traffic before
* deleting.
*/
@Deprecated(since = "0.13.0", forRemoval = true)
@RestController
@RequestMapping("/mcp")
@RequiredArgsConstructor
@Slf4j
public class McpController {
private final AccessControlService accessControlService;
private final QueryExecutorService queryExecutorService;
private final ExplainPlanService explainPlanService;
private final McpSqlGuardService sqlGuardService;
@PostMapping("/query-readonly")
public ResponseEntity<?> executeReadOnlyQuery(@RequestBody McpReadOnlyQueryRequest request) {
log.warn(
"DEPRECATED endpoint /mcp/query-readonly called for connection {}. "
+ "Upgrade @deepsql/mcp to 0.13.0+; this endpoint will be removed in 0.14.0.",
request == null ? null : request.getConnectionId()
);
if (request.getConnectionId() == null || request.getConnectionId().isBlank()) {
return ResponseEntity.badRequest().body(Map.of("message", "Connection ID is required"));
}
McpSqlGuardService.ValidationOutcome validation =
sqlGuardService.validateReadOnlySql(request.getQuery(), true);
if (!validation.ok()) {
return ResponseEntity.badRequest().body(Map.of("message", validation.reason()));
}
try {
accessControlService.assertCanUseChatEditor(request.getConnectionId());
QueryRequest queryRequest = new QueryRequest();
queryRequest.setQuery(validation.normalizedQuery());
queryRequest.setLimit(request.getLimit());
queryRequest.setTimeoutSeconds(request.getTimeoutSeconds());
queryRequest.setExecutionOrigin(QueryExecutionOrigin.MCP);
QueryResult result = queryExecutorService.executeQuery(
request.getConnectionId(),
queryRequest,
QueryExecutionContext.mcp(
accessControlService.getCurrentUsername(),
accessControlService.isCurrentUserAdmin()
)
);
return ResponseEntity.ok(Map.of(
"success", true,
"result", result,
"queryType", validation.firstKeyword()
));
} catch (QueryExecutionPolicyException e) {
return ResponseEntity.status(e.getHttpStatus())
.body(Map.of(
"success", false,
"message", e.getMessage(),
"errorCode", e.getErrorCode()
));
} catch (org.springframework.web.server.ResponseStatusException e) {
throw e;
} catch (Exception e) {
if (e instanceof ResponseStatusException responseStatusException) {
return ResponseEntity.status(responseStatusException.getStatusCode())
.body(Map.of("message", responseStatusException.getReason()));
}
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(Map.of("message", "Query execution failed: " + e.getMessage()));
}
}
@PostMapping("/explain-readonly")
public ResponseEntity<?> explainReadOnlyQuery(@RequestBody McpReadOnlyExplainRequest request) {
log.warn(
"DEPRECATED endpoint /mcp/explain-readonly called for connection {}. "
+ "Upgrade @deepsql/mcp to 0.13.0+ and use /explain/analyze; "
+ "this endpoint will be removed in 0.14.0.",
request == null ? null : request.getConnectionId()
);
if (request.getConnectionId() == null || request.getConnectionId().isBlank()) {
return ResponseEntity.badRequest().body(Map.of("message", "Connection ID is required"));
}
McpSqlGuardService.ValidationOutcome validation =
sqlGuardService.validateReadOnlySql(request.getQuery(), false);
if (!validation.ok()) {
return ResponseEntity.badRequest().body(Map.of("message", validation.reason()));
}
try {
accessControlService.assertCanUseChatEditor(request.getConnectionId());
ExplainPlanAnalysis analysis = explainPlanService.analyzeQuery(
request.getConnectionId(),
validation.normalizedQuery(),
false
);
return ResponseEntity.ok(analysis);
} catch (org.springframework.web.server.ResponseStatusException e) {
throw e;
} catch (Exception e) {
if (e instanceof ResponseStatusException responseStatusException) {
return ResponseEntity.status(responseStatusException.getStatusCode())
.body(Map.of("message", responseStatusException.getReason()));
}
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(Map.of("message", "Explain analysis failed: " + e.getMessage()));
}
}
}