Autopsy 4.23.0
Graphical digital forensics platform for The Sleuth Kit and other tools.
McpProtocolHandler.java
Go to the documentation of this file.
1/*
2 * Autopsy Forensic Browser
3 *
4 * Copyright 2024 Basis Technology Corp.
5 * Contact: carrier <at> sleuthkit <dot> org
6 *
7 * Licensed under the Apache License, Version 2.0 (the "License");
8 * you may not use this file except in compliance with the License.
9 * You may obtain a copy of the License at
10 *
11 * http://www.apache.org/licenses/LICENSE-2.0
12 *
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS,
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 * See the License for the specific language governing permissions and
17 * limitations under the License.
18 */
19package org.sleuthkit.autopsy.mcp;
20
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;
26
27import java.util.LinkedHashMap;
28import java.util.Map;
29import java.util.logging.Level;
30import java.util.logging.Logger;
31
39class McpProtocolHandler {
40
41 private static final Logger logger = Logger.getLogger(McpProtocolHandler.class.getName());
42
43 // JSON-RPC 2.0 reserved error codes (package-private for reuse in McpServer)
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;
47
48 // Used solely for tools/list — listTools() has no case dependency.
49 private static final TskQueryService TOOLS_LIST_SERVICE = new TskQueryService(null, null);
50
51 private volatile TskQueryService queryService; // null = no case open
52 private final ObjectMapper mapper = new ObjectMapper();
53
54 McpProtocolHandler() { }
55
56 void setQueryService(TskQueryService qs) {
57 this.queryService = qs;
58 }
59
60 void clearQueryService() {
61 this.queryService = null;
62 }
63
68 public String handle(String requestJson) throws Exception {
69 // id defaults to NullNode so parse errors return a conforming response
70 // even when the request cannot be read at all (JSON-RPC spec §5).
71 JsonNode id = NullNode.getInstance();
72 try {
73 JsonNode request = mapper.readTree(requestJson);
74 String method = request.path("method").asText();
75 JsonNode params = request.path("params");
76 // Preserve the id as a JsonNode so its original type (number, string, null,
77 // or absent) is returned unchanged in the response, as the JSON-RPC spec requires.
78 id = request.has("id") ? request.get("id") : NullNode.getInstance();
79
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);
85 };
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) {
91 // Expected protocol-level errors (unknown method/tool, bad params, no case open)
92 // are client-visible in the response — no need to flood the Autopsy log.
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());
97 }
98 }
99
100 private Object dispatchToolCall(JsonNode params) throws Exception {
101 String toolName = params.path("name").asText();
102 JsonNode args = params.path("arguments");
103
104 // Tools that work regardless of whether a case is open
105 if ("get_server_status".equals(toolName)) {
106 return buildServerStatus();
107 }
108 if ("get_case_summary".equals(toolName) && queryService == null) {
109 return wrapWithCaseId(
110 Map.of("message", "No case is currently open in Autopsy."), null);
111 }
112
113 TskQueryService qs = queryService;
114 if (qs == null) {
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);
118 }
119
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);
139 };
140
141 return wrapWithCaseId(toolResult, qs.getCaseName());
142 }
143
144 private Map<String, Object> wrapWithCaseId(Object result, String caseId) {
145 Map<String, Object> wrapper = new LinkedHashMap<>();
146 wrapper.put("caseId", caseId); // null when no case is open
147 wrapper.put("result", result);
148 return wrapper;
149 }
150
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);
158 if (caseOpen) {
159 status.put("caseName", qs.getCaseName());
160 }
161 return status;
162 }
163
164 private Object handleInitialize() {
165 return Map.of(
166 "protocolVersion", "2024-11-05",
167 "serverInfo", Map.of("name", "autopsy-mcp", "version", "1.0.0"),
168 "capabilities", Map.of("tools", Map.of())
169 );
170 }
171
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);
178 }
179
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);
189 }
190}

Copyright © 2012-2024 Sleuth Kit Labs. Generated on:
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.