Autopsy 4.23.0
Graphical digital forensics platform for The Sleuth Kit and other tools.
McpServer.java
Go to the documentation of this file.
1/*
2 * Autopsy
3 *
4 * Copyright 2026 Sleuth Kit Labs
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.databind.ObjectMapper;
22import io.javalin.Javalin;
23import io.javalin.http.Context;
24import static io.javalin.apibuilder.ApiBuilder.*;
25import org.sleuthkit.autopsy.casemodule.Case;
26
27import java.io.IOException;
28import java.io.InputStream;
29import java.io.OutputStream;
30import java.nio.file.Files;
31import java.nio.file.Path;
32import java.nio.file.StandardOpenOption;
33import java.security.SecureRandom;
34import java.util.Base64;
35import java.util.LinkedHashMap;
36import java.util.Map;
37import java.util.Properties;
38import java.util.logging.Level;
39import java.util.logging.Logger;
40
51public class McpServer {
52
53 private static final Logger logger = Logger.getLogger(McpServer.class.getName());
54 private static final ObjectMapper MAPPER = new ObjectMapper();
55 private static final int DEFAULT_PORT = 8743;
56 private static final String CONFIG_FILE_NAME = "mcp-config.properties";
57 private static final String PORT_PROPERTY = "port";
58
59 private final String authToken;
60 private final McpProtocolHandler protocolHandler;
61 private Javalin app;
62
63 public McpServer() {
64 this.authToken = generateToken();
65 this.protocolHandler = new McpProtocolHandler();
66 }
67
72 public void updateCase(Case openedCase) {
73 protocolHandler.setQueryService(
74 new TskQueryService(openedCase.getSleuthkitCase(), openedCase.getDisplayName()));
75 }
76
82 public void clearCase() {
83 protocolHandler.clearQueryService();
84 }
85
86 public void start() {
87 app = Javalin.create(config -> {
88 config.routes.apiBuilder(() -> {
89 // Auth filter — every request must have valid Bearer token
90 before(ctx -> {
91 String auth = ctx.header("Authorization");
92 if (auth == null || !auth.equals("Bearer " + authToken)) {
93 ctx.status(401).result("Unauthorized");
94 ctx.skipRemainingHandlers();
95 }
96 });
97
98 // MCP endpoint
99 post("/mcp", this::handleMcpRequest);
100 });
101 });
102
103 int port = readOrCreateConfigPort();
104 app.start("127.0.0.1", port); // localhost only — never 0.0.0.0
105 try {
107 } catch (IOException ex) {
108 app.stop();
109 throw new RuntimeException("MCP server started but failed to write token file — aborting", ex);
110 }
111 }
112
113 public void stop() {
114 if (app != null) {
115 app.stop();
116 app = null;
117 }
119 }
120
121 private void handleMcpRequest(Context ctx) {
122 try {
123 String requestBody = ctx.body();
124 String response = protocolHandler.handle(requestBody);
125 ctx.contentType("application/json").result(response);
126 } catch (Exception ex) {
127 logger.log(Level.SEVERE, "MCP protocol handler threw unexpectedly", ex);
128 String msg = ex.getMessage() != null ? ex.getMessage() : "Internal error";
129 Map<String, Object> errorDetail = new LinkedHashMap<>();
130 errorDetail.put("code", McpProtocolHandler.ERR_INTERNAL_ERROR);
131 errorDetail.put("message", "Internal error");
132 errorDetail.put("data", msg);
133 Map<String, Object> envelope = new LinkedHashMap<>();
134 envelope.put("jsonrpc", "2.0");
135 envelope.put("error", errorDetail);
136 envelope.put("id", null);
137 String body;
138 try {
139 body = MAPPER.writeValueAsString(envelope);
140 } catch (Exception jsonEx) {
141 logger.log(Level.SEVERE, "Failed to serialize MCP error response", jsonEx);
142 body = "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":" + McpProtocolHandler.ERR_INTERNAL_ERROR + ",\"message\":\"Internal error\"},\"id\":null}";
143 }
144 ctx.status(500).contentType("application/json").result(body);
145 }
146 }
147
155 Path configPath = getMcpDir().resolve(CONFIG_FILE_NAME);
156 Properties props = new Properties();
157
158 if (Files.exists(configPath)) {
159 try (InputStream in = Files.newInputStream(configPath)) {
160 props.load(in);
161 String portStr = props.getProperty(PORT_PROPERTY, "").trim();
162 int port = Integer.parseInt(portStr);
163 if (port > 0 && port <= 65535) {
164 return port;
165 }
166 logger.log(Level.WARNING, "Invalid port in MCP config ({0}), using default {1}",
167 new Object[]{portStr, DEFAULT_PORT});
168 } catch (IOException | NumberFormatException ex) {
169 logger.log(Level.WARNING, "Could not read MCP config port, using default", ex);
170 }
171 } else {
172 // Create the file so users know it exists and can edit it.
173 try {
174 Files.createDirectories(configPath.getParent());
175 props.setProperty(PORT_PROPERTY, String.valueOf(DEFAULT_PORT));
176 try (OutputStream out = Files.newOutputStream(configPath,
177 StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
178 props.store(out,
179 "Autopsy MCP server configuration\n"
180 + "# Change the port number below if it conflicts with another application.\n"
181 + "# Restart Autopsy after editing this file.");
182 }
183 } catch (IOException ex) {
184 logger.log(Level.WARNING, "Could not create MCP config file, using default port", ex);
185 }
186 }
187 return DEFAULT_PORT;
188 }
189
190 private String generateToken() {
191 byte[] bytes = new byte[32];
192 new SecureRandom().nextBytes(bytes);
193 return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
194 }
195
196 private Path getMcpDir() {
197 String localAppData = System.getenv("LOCALAPPDATA");
198 if (localAppData != null && !localAppData.isEmpty()) {
199 return Path.of(localAppData, "autopsy", "mcp");
200 }
201 return Path.of(System.getProperty("user.home"), "AppData", "Local", "autopsy", "mcp");
202 }
203
204 private Path getTokenPath() {
205 return getMcpDir().resolve("mcp-token");
206 }
207
208 private void writeTokenFile() throws IOException {
209 Path tokenPath = getTokenPath();
210 Files.createDirectories(tokenPath.getParent());
211 Files.writeString(tokenPath, authToken);
212 tokenPath.toFile().deleteOnExit();
213 }
214
215 private void deleteTokenFile() {
216 try {
217 Files.deleteIfExists(getTokenPath());
218 } catch (IOException ex) {
219 logger.log(Level.WARNING, "Failed to delete MCP token file", ex);
220 }
221 }
222}
synchronized static Logger getLogger(String name)
Definition Logger.java:124
static final ObjectMapper MAPPER
void updateCase(Case openedCase)
final McpProtocolHandler protocolHandler
static final String CONFIG_FILE_NAME
static final String PORT_PROPERTY

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