19package org.sleuthkit.autopsy.mcp;
21import com.fasterxml.jackson.core.JsonProcessingException;
22import com.fasterxml.jackson.databind.JsonNode;
23import com.fasterxml.jackson.databind.ObjectMapper;
24import com.fasterxml.jackson.databind.node.NullNode;
25import com.fasterxml.jackson.databind.node.ObjectNode;
27import java.util.LinkedHashMap;
29import java.util.logging.Level;
30import java.util.logging.Logger;
39class McpProtocolHandler {
41 private static final Logger logger = Logger.getLogger(McpProtocolHandler.class.getName());
44 static final int ERR_PARSE_ERROR = -32700;
45 static final int ERR_METHOD_NOT_FOUND = -32601;
46 static final int ERR_INTERNAL_ERROR = -32603;
49 private static final TskQueryService TOOLS_LIST_SERVICE =
new TskQueryService(
null,
null);
51 private volatile TskQueryService queryService;
52 private final ObjectMapper mapper =
new ObjectMapper();
54 McpProtocolHandler() { }
56 void setQueryService(TskQueryService qs) {
57 this.queryService = qs;
60 void clearQueryService() {
61 this.queryService =
null;
68 public String handle(String requestJson)
throws Exception {
71 JsonNode
id = NullNode.getInstance();
73 JsonNode request = mapper.readTree(requestJson);
74 String method = request.path(
"method").asText();
75 JsonNode params = request.path(
"params");
78 id = request.has(
"id") ? request.get(
"id") : NullNode.getInstance();
80 Object result =
switch (method) {
81 case "tools/list" -> TOOLS_LIST_SERVICE.listTools();
82 case "tools/call" -> dispatchToolCall(params);
83 case "initialize" -> handleInitialize();
84 default ->
throw new McpException(
"Unknown method: " + method, McpException.ERR_METHOD_NOT_FOUND);
86 return buildSuccess(
id, result);
87 }
catch (JsonProcessingException ex) {
88 logger.log(Level.WARNING,
"MCP request contained malformed JSON", ex);
89 return buildError(NullNode.getInstance(), ERR_PARSE_ERROR,
"Parse error");
90 }
catch (McpException ex) {
93 return buildError(
id, ex.getJsonRpcCode(), ex.getMessage());
94 }
catch (Exception ex) {
95 logger.log(Level.SEVERE,
"Unexpected error handling MCP request", ex);
96 return buildError(
id, ERR_INTERNAL_ERROR, ex.getMessage());
100 private Object dispatchToolCall(JsonNode params)
throws Exception {
101 String toolName = params.path(
"name").asText();
102 JsonNode args = params.path(
"arguments");
105 if (
"get_server_status".equals(toolName)) {
106 return buildServerStatus();
108 if (
"get_case_summary".equals(toolName) && queryService ==
null) {
109 return wrapWithCaseId(
110 Map.of(
"message",
"No case is currently open in Autopsy."),
null);
113 TskQueryService qs = queryService;
115 throw new McpException(
116 "No case is currently open in Autopsy. Open a case first to use MCP tools.",
117 McpException.ERR_INTERNAL_ERROR);
120 Object toolResult =
switch (toolName) {
121 case "query_files" -> qs.queryFiles(args);
122 case "query_data_artifacts" -> qs.queryDataArtifacts(args);
123 case "query_analysis_results" -> qs.queryAnalysisResults(args);
124 case "get_hosts" -> qs.getHosts();
125 case "query_data_sources" -> qs.queryDataSources();
126 case "get_data_source_tree" -> qs.getDataSourceTree(args);
127 case "get_case_summary" -> qs.getCaseSummary();
128 case "get_file_content" -> qs.getFileContent(args);
129 case "query_tags" -> qs.queryTags(args);
130 case "query_timeline" -> qs.queryTimeline(args);
131 case "summarize_timeline" -> qs.summarizeTimeline(args);
132 case "get_os_accounts" -> qs.getOsAccounts();
133 case "get_communications_accounts" -> qs.getCommunicationsAccounts(args);
134 case "get_account_relationships" -> qs.getAccountRelationships(args);
135 case "get_object_children" -> qs.getObjectChildren(args);
136 case "list_reports" -> qs.listReports();
137 case "get_report_content" -> qs.getReportContent(args);
138 default ->
throw new McpException(
"Unknown tool: " + toolName, McpException.ERR_METHOD_NOT_FOUND);
141 return wrapWithCaseId(toolResult, qs.getCaseName());
144 private Map<String, Object> wrapWithCaseId(Object result, String caseId) {
145 Map<String, Object> wrapper =
new LinkedHashMap<>();
146 wrapper.put(
"caseId", caseId);
147 wrapper.put(
"result", result);
151 private Map<String, Object> buildServerStatus() {
152 TskQueryService qs = queryService;
153 boolean caseOpen = qs !=
null;
154 Map<String, Object> status =
new LinkedHashMap<>();
155 status.put(
"server",
"autopsy-mcp");
156 status.put(
"status",
"running");
157 status.put(
"caseOpen", caseOpen);
159 status.put(
"caseName", qs.getCaseName());
164 private Object handleInitialize() {
166 "protocolVersion",
"2024-11-05",
167 "serverInfo", Map.of(
"name",
"autopsy-mcp",
"version",
"1.0.0"),
168 "capabilities", Map.of(
"tools", Map.of())
172 private String buildSuccess(JsonNode
id, Object result)
throws Exception {
173 ObjectNode response = mapper.createObjectNode();
174 response.put(
"jsonrpc",
"2.0");
175 response.set(
"id",
id);
176 response.set(
"result", mapper.valueToTree(result));
177 return mapper.writeValueAsString(response);
180 private String buildError(JsonNode
id,
int code, String message)
throws Exception {
181 ObjectNode response = mapper.createObjectNode();
182 response.put(
"jsonrpc",
"2.0");
183 response.set(
"id",
id);
184 ObjectNode error = mapper.createObjectNode();
185 error.put(
"code", code);
186 error.put(
"message", message);
187 response.set(
"error", error);
188 return mapper.writeValueAsString(response);