mcp-dfir 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcp-dfir
3
+ Version: 0.1.0
4
+ Summary: MCP Server for Performing Memory and Disk Forensics
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: attrs
7
+ Requires-Dist: loguru
8
+ Requires-Dist: mcp
9
+ Requires-Dist: python_on_whales
10
+ Requires-Dist: requests
11
+ Provides-Extra: dev
12
+ Requires-Dist: build; extra == "dev"
13
+ Requires-Dist: mcp[cli]; extra == "dev"
14
+ Requires-Dist: pytest; extra == "dev"
15
+ Requires-Dist: ruff; extra == "dev"
16
+ Requires-Dist: twine; extra == "dev"
17
+ Requires-Dist: uv; extra == "dev"
@@ -0,0 +1,111 @@
1
+ # MCP DFIR
2
+
3
+ MCP DFIR (Digital Forensics Incident Response) is an MCP server that gives AI agents the tools to perform memory, disk, and artifact forensics. It runs forensics tools inside an isolated Docker container and exposes them over the MCP stdio transport.
4
+
5
+ [Example report](/examples/tryhackme_volatility/analysis/analyst_notes/Case%20002%20-%20Complete%20Incident%20Analysis%20Report.md) based on the TryHackMe Volatility Essentials room.
6
+
7
+ # Features
8
+
9
+ - Memory forensics via [Volatility3](https://github.com/volatilityfoundation/volatility3)
10
+ - Disk forensics via [The Sleuth Kit](https://www.sleuthkit.org/)
11
+ - File search with `strings`, `grep`, and `find`
12
+ - Archive extraction with `unzip` and `tar`
13
+ - Automatic Windows symbol downloading and conversion
14
+ - Linux symbol search and downloading from [Abyss-W4tcher/volatility3-symbols](https://github.com/Abyss-W4tcher/volatility3-symbols)
15
+ - Persistent command history — the agent never re-runs commands already completed
16
+ - Analyst notes — the agent records findings in structured markdown notes
17
+
18
+ # Requirements
19
+
20
+ - [Docker](https://www.docker.com/) must be installed and running
21
+
22
+ # Installation
23
+
24
+ ```bash
25
+ pip install mcp-dfir
26
+ ```
27
+
28
+ # Setup
29
+
30
+ ## Claude Desktop config
31
+
32
+ Add the following to your Claude Desktop `claude_desktop_config.json`:
33
+
34
+ ```json
35
+ {
36
+ "mcpServers": {
37
+ "mcp-dfir": {
38
+ "command": "mcp-dfir"
39
+ }
40
+ }
41
+ }
42
+ ```
43
+
44
+ ## Case directory layout
45
+
46
+ The MCP server expects evidence and artifacts to be organized under a case directory:
47
+
48
+ ```
49
+ my_case/
50
+ ├── evidence/ # Place memory dumps, disk images, etc. here (read-only)
51
+ ├── artifacts/ # Output location for extracted files (read-write)
52
+ ├── symbols/ # Volatility symbol files (auto-populated on download)
53
+ └── analysis/ # Command history and analyst notes (auto-generated)
54
+ ```
55
+
56
+ Place all evidence files (`.mem`, `.img`, `.e01`, etc.) inside the `evidence/` directory before starting a session.
57
+
58
+ # Args
59
+
60
+ ```
61
+ usage: mcp-dfir [-h] [--version] [-d CASE_DIR]
62
+
63
+ options:
64
+ -h, --help show this help message and exit
65
+ --version show program's version number and exit
66
+ -d, --case-dir CASE_DIR
67
+ Working directory for the case, default: current directory
68
+ ```
69
+
70
+ # MCP Tools
71
+
72
+ | Tool | Description |
73
+ | ---------------------------- | -------------------------------------------------------------------- |
74
+ | `run_volatility_command` | Runs Volatility3 (`vol`) for memory forensics |
75
+ | `download_windows_symbol` | Downloads and converts Windows PDB symbols for Volatility3 |
76
+ | `download_linux_symbol` | Downloads a Linux symbol file from Abyss-W4tcher/volatility3-symbols |
77
+ | `search_linux_symbols` | Searches the Linux symbol map by regex to find matching symbol files |
78
+ | `run_sleuthkit_command` | Runs any Sleuth Kit tool for disk forensics |
79
+ | `run_strings` | Runs `strings` on a file |
80
+ | `run_grep` | Runs `grep` on files |
81
+ | `run_find` | Runs `find` to search for files |
82
+ | `list_files` | Lists files in `evidence`, `artifacts`, or `symbols` |
83
+ | `unzip_file` | Extracts a zip archive into `/artifacts` |
84
+ | `untar_file` | Extracts a tar archive into `/artifacts` |
85
+ | `get_hash` | Computes a hash of a file |
86
+ | `get_command_history` | Returns all previously run commands |
87
+ | `get_command_result` | Returns the output of a specific past command |
88
+ | `show_analyst_notes_summary` | Lists all analyst notes |
89
+ | `show_analyst_note` | Retrieves the content of a specific analyst note |
90
+ | `add_analyst_note` | Creates a new analyst note |
91
+ | `update_analyst_note` | Updates an existing analyst note |
92
+
93
+ # Build from source
94
+
95
+ ```bash
96
+ # Create virtual environment
97
+ python3 -m venv .venv
98
+
99
+ # Activate (Linux/macOS)
100
+ source .venv/bin/activate
101
+ # Activate (Windows)
102
+ .venv/Scripts/activate.ps1
103
+
104
+ # Install dependencies
105
+ pip install .[dev]
106
+
107
+ # Run
108
+ mcp-dfir --case-dir /path/to/your/case
109
+ # or
110
+ python3 -m mcp_dfir --case-dir /path/to/your/case
111
+ ```
File without changes
@@ -0,0 +1,110 @@
1
+ """Defines the config object"""
2
+
3
+ # Standard libraries
4
+ from pathlib import Path
5
+
6
+ # Third-party libraries
7
+ from attrs import define, field, validators
8
+
9
+ # Project libraries
10
+ from mcp_dfir.constants import VERSION
11
+
12
+
13
+ @define
14
+ class DockerVolume:
15
+ name: str = field(validator=validators.instance_of(str))
16
+ internal_path: str = field(validator=validators.instance_of(str))
17
+ external_path: Path = field(validator=validators.instance_of(Path))
18
+ permissions: str = field(validator=validators.instance_of(str))
19
+
20
+
21
+ @define
22
+ class DockerConfig:
23
+ container_name: str = field(validator=validators.instance_of(str))
24
+ image_name: str = field(validator=validators.instance_of(str))
25
+
26
+
27
+ @define
28
+ class Config:
29
+ working_directory: Path = field(default=Path.cwd(), validator=validators.instance_of(Path))
30
+
31
+ # Directories
32
+ analysis_directory: Path = field(init=False, validator=validators.instance_of(Path))
33
+ artifact_directory: Path = field(init=False, validator=validators.instance_of(Path))
34
+ symbols_directory: Path = field(init=False, validator=validators.instance_of(Path))
35
+ evidence_directory: Path = field(init=False, validator=validators.instance_of(Path))
36
+
37
+ # Command history
38
+ command_history_json: Path = field(init=False, validator=validators.instance_of(Path))
39
+ command_record_directory: Path = field(init=False, validator=validators.instance_of(Path))
40
+
41
+ # Analyst notes
42
+ analyst_notes_list_json: Path = field(init=False, validator=validators.instance_of(Path))
43
+ analyst_notes_directory: Path = field(init=False, validator=validators.instance_of(Path))
44
+
45
+ # Symbols
46
+ windows_symbols_directory: Path = field(init=False, validator=validators.instance_of(Path))
47
+ linux_symbols_directory: Path = field(init=False, validator=validators.instance_of(Path))
48
+ linux_symbol_map_json: Path = field(init=False, validator=validators.instance_of(Path))
49
+
50
+ # Docker
51
+ docker_volumes: list[DockerVolume] = field(
52
+ init=False,
53
+ validator=validators.deep_iterable(
54
+ member_validator=validators.instance_of(DockerVolume), iterable_validator=validators.instance_of(list)
55
+ ),
56
+ )
57
+ docker_config: DockerConfig = field(init=False, validator=validators.instance_of(DockerConfig))
58
+
59
+ def load(self, working_directory: Path):
60
+
61
+ # Directories
62
+ self.working_directory = working_directory
63
+
64
+ self.analysis_directory = self.working_directory / "analysis"
65
+ self.analysis_directory.mkdir(mode=500, parents=False, exist_ok=True)
66
+
67
+ self.artifact_directory = self.working_directory / "artifacts"
68
+ self.artifact_directory.mkdir(mode=500, parents=False, exist_ok=True)
69
+
70
+ self.symbols_directory = self.working_directory / "symbols"
71
+ self.symbols_directory.mkdir(mode=500, parents=False, exist_ok=True)
72
+
73
+ self.evidence_directory = self.working_directory / "evidence"
74
+ self.evidence_directory.mkdir(mode=500, parents=False, exist_ok=True)
75
+
76
+ # Command history
77
+ self.command_history_json = self.analysis_directory / "command_history.json"
78
+ self.command_record_directory = self.analysis_directory / "command_history"
79
+
80
+ # Analyst notes
81
+ self.analyst_notes_list_json = self.analysis_directory / "analyst_notes.json"
82
+ self.analyst_notes_directory = self.analysis_directory / "analyst_notes"
83
+
84
+ # Symbols
85
+ self.windows_symbols_directory = self.symbols_directory / "windows"
86
+ self.linux_symbols_directory = self.symbols_directory / "linux"
87
+ self.linux_symbol_map_json = self.linux_symbols_directory / "symbol_map.json"
88
+
89
+ # Docker
90
+ self.docker_volumes = [
91
+ DockerVolume(
92
+ name="evidence", internal_path="/evidence", external_path=self.evidence_directory, permissions="ro"
93
+ ),
94
+ DockerVolume(
95
+ name="analysis", internal_path="/analysis", external_path=self.analysis_directory, permissions="ro"
96
+ ),
97
+ DockerVolume(
98
+ name="symbols", internal_path="/symbols", external_path=self.symbols_directory, permissions="rw"
99
+ ),
100
+ DockerVolume(
101
+ name="artifacts", internal_path="/artifacts", external_path=self.artifact_directory, permissions="rw"
102
+ ),
103
+ ]
104
+ self.docker_config = DockerConfig(
105
+ container_name=f"{self.working_directory.name.replace(' ', '_')}-forensics",
106
+ image_name=f"androsh7/forensics-docker:{VERSION}",
107
+ )
108
+
109
+
110
+ config = Config()
@@ -0,0 +1,11 @@
1
+ """Defines constants"""
2
+
3
+ VERSION = "0.1.0"
4
+
5
+ # Volatility symbols
6
+ WINDOWS_SYMBOL_SERVER = "https://msdl.microsoft.com/download/symbols"
7
+ LINUX_SYMBOL_REPO_BASE_URL = "https://raw.githubusercontent.com/Abyss-W4tcher/volatility3-symbols/refs/heads/master"
8
+ LINUX_SYMBOL_MAP_SOURCE = f"{LINUX_SYMBOL_REPO_BASE_URL}/banners/banners_plain.json"
9
+
10
+ # Command output limit
11
+ COMMAND_MAX_OUTPUT_BYTES = 25 * 1024 # 2 KB
@@ -0,0 +1,117 @@
1
+ """Defines the data manager class"""
2
+
3
+ # Standard libraries
4
+ import json
5
+
6
+ # Third-party libraries
7
+ from pydantic import BaseModel, Field
8
+
9
+ # Project libraries
10
+ from mcp_dfir.config import config
11
+ from mcp_dfir.tools.models import AnalystNote, AnalystNoteSummary, CommandRecord, CommandRecordSummary
12
+
13
+
14
+ class CommandHistoryManager(BaseModel):
15
+ command_summary_list: list[CommandRecordSummary] = Field(default_factory=list, init=False)
16
+
17
+ def model_post_init(self, __context):
18
+ # Load commands
19
+ if config.command_history_json.exists():
20
+ self.load()
21
+ config.command_record_directory.mkdir(mode=500, parents=True, exist_ok=True)
22
+
23
+ def dump(self, command: CommandRecord | None, command_number: int | None = None):
24
+ # Write to command history
25
+ with open(file=config.command_history_json, mode="w", encoding="utf-8") as command_summary_file:
26
+ json.dump(
27
+ [record.model_dump() for record in self.command_summary_list],
28
+ command_summary_file,
29
+ indent=4,
30
+ )
31
+
32
+ # Write to command file
33
+ if command is not None and command_number is not None:
34
+ with open(
35
+ file=config.command_record_directory / f"{command_number}.txt", mode="w", encoding="utf-8"
36
+ ) as command_file:
37
+ command_file.write(command.result)
38
+
39
+ def load(self):
40
+ with open(file=config.command_history_json, encoding="utf-8") as command_file:
41
+ for command_summary_dict in json.load(command_file):
42
+ self.command_summary_list.append(CommandRecordSummary.model_validate(command_summary_dict))
43
+
44
+ def add_command(self, command_record: CommandRecord) -> CommandRecordSummary:
45
+ number = len(self.command_summary_list) + 1
46
+ summary = command_record.summary(number=number)
47
+ self.command_summary_list.append(summary)
48
+ self.dump(command_record, number)
49
+ return summary
50
+
51
+ def get_command(self, command_number: int) -> CommandRecord:
52
+ for command in self.command_summary_list:
53
+ if command.number == command_number:
54
+ with open(file=config.command_record_directory / f"{command_number}.txt", encoding="utf-8") as file:
55
+ return CommandRecord(
56
+ command=command.command,
57
+ date_run=command.date_run,
58
+ result=file.read(),
59
+ exit_code=command.exit_code,
60
+ )
61
+ raise KeyError(f"Could not find command with number {command_number}")
62
+
63
+
64
+ class AnalystNoteManager(BaseModel):
65
+ note_list: list[AnalystNoteSummary] = Field(default_factory=list, init=False)
66
+
67
+ def model_post_init(self, __context):
68
+ if config.analyst_notes_list_json.exists():
69
+ self.load()
70
+ config.analyst_notes_directory.mkdir(mode=500, exist_ok=True)
71
+
72
+ def dump(self, note: AnalystNote | None):
73
+ # Update note summary
74
+ with open(file=config.analyst_notes_list_json, mode="w", encoding="utf-8") as note_summary_file:
75
+ json.dump(
76
+ [note.model_dump() for note in self.note_list],
77
+ note_summary_file,
78
+ indent=4,
79
+ )
80
+
81
+ # Write to note file
82
+ if note is not None:
83
+ with open(
84
+ file=config.analyst_notes_directory / f"{note.title}.md", mode="w", encoding="utf-8"
85
+ ) as note_file:
86
+ note_file.write(note.description)
87
+
88
+ def load(self):
89
+ with open(file=config.analyst_notes_list_json, encoding="utf-8") as note_file:
90
+ for note_dict in json.load(note_file):
91
+ self.note_list.append(AnalystNote.model_validate(note_dict))
92
+
93
+ def add_note(self, note: AnalystNote) -> AnalystNote:
94
+ self.note_list.append(note.summary())
95
+ self.dump(note)
96
+ return note
97
+
98
+ def update_note(self, title: str, note: AnalystNote) -> AnalystNote:
99
+ for index, note in enumerate(self.note_list):
100
+ if note.title == title:
101
+ self.note_list[index] = note.summary()
102
+ self.dump(note)
103
+ return note
104
+ raise KeyError(f"No note with title '{title}'")
105
+
106
+ def get_note(self, title: str) -> AnalystNote:
107
+ for note in self.note_list:
108
+ if note.title == title:
109
+ with open(file=config.analyst_notes_directory / f"{title}.md", encoding="utf-8") as note_file:
110
+ return AnalystNote(
111
+ title=note.title, status=note.status, tags=note.tags, description=note_file.read()
112
+ )
113
+ raise KeyError(f"No note with title '{title}'")
114
+
115
+
116
+ command_history_manager = CommandHistoryManager()
117
+ analyst_note_manager = AnalystNoteManager()
@@ -0,0 +1,91 @@
1
+ """Defines all functions to run commands on the docker container"""
2
+
3
+ # Third-party libraries
4
+ from attrs import define, field, validators
5
+ from loguru import logger
6
+ from mcp.server.fastmcp import Context
7
+ from python_on_whales import Container, DockerClient
8
+ from python_on_whales.exceptions import DockerException, NoSuchContainer
9
+
10
+ # Project libraries
11
+ from mcp_dfir.config import config
12
+ from mcp_dfir.data_manager import command_history_manager
13
+ from mcp_dfir.tools.models import CommandRecord, CommandRecordSummary
14
+
15
+
16
+ @define
17
+ class DockerManager:
18
+ docker: DockerClient = field(validator=validators.instance_of(DockerClient), init=False)
19
+ container: Container = field(validator=validators.optional(validators.instance_of(Container)), init=False)
20
+
21
+ def __attrs_post_init__(self):
22
+ self.docker = DockerClient()
23
+ self.container = None
24
+
25
+ def start_forensic_container(self):
26
+ logger.info("Starting forensics docker container")
27
+
28
+ try:
29
+ self.container = self.docker.container.inspect(config.docker_config.container_name)
30
+
31
+ if self.container.state.running:
32
+ logger.info("Forensics container already running")
33
+ return
34
+
35
+ logger.info("Forensics container exists but is stopped, starting it")
36
+ self.docker.container.start(config.docker_config.container_name)
37
+ return
38
+
39
+ except NoSuchContainer:
40
+ logger.info("Forensics container does not exist, creating one")
41
+
42
+ volumes = [
43
+ (volume_mount.external_path, volume_mount.internal_path, volume_mount.permissions)
44
+ for volume_mount in list(config.docker_volumes)
45
+ ]
46
+ self.container = self.docker.run(
47
+ config.docker_config.image_name,
48
+ name=config.docker_config.container_name,
49
+ detach=True,
50
+ remove=False,
51
+ volumes=volumes,
52
+ networks=["none"],
53
+ )
54
+
55
+ def stop_forensic_container(self):
56
+ if self.container is not None:
57
+ logger.info("Stopping forensics docker container")
58
+ self.container.stop()
59
+
60
+ def clear_forensic_container(self):
61
+ if self.container is not None:
62
+ logger.info("Clearing forensics docker container")
63
+ self.container.stop()
64
+ self.container.remove()
65
+ self.container = None
66
+
67
+ def exec_stream(self, ctx: Context, command_list: list[str]) -> CommandRecordSummary:
68
+ if self.container is None:
69
+ raise RuntimeError("No docker container is currently running")
70
+
71
+ ctx.report_progress(f"Running command {' '.join(command_list)}")
72
+ output = []
73
+ exit_code = 0
74
+ try:
75
+ for stream, data in self.docker.execute(self.container, command_list, stream=True):
76
+ if stream == "stdout":
77
+ output.append(data.decode())
78
+ except DockerException as ex:
79
+ exit_code = ex.return_code if ex.return_code is not None else 1
80
+ output.append(str(ex))
81
+ result = "".join(output)
82
+ return command_history_manager.add_command(
83
+ CommandRecord(
84
+ command=" ".join(command_list),
85
+ result=result,
86
+ exit_code=exit_code,
87
+ )
88
+ )
89
+
90
+
91
+ docker_manager = DockerManager()
@@ -0,0 +1,67 @@
1
+ """Runner for llm_forensics"""
2
+
3
+ # Standard libraries
4
+ import argparse
5
+ import shutil
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ # Project libraries
10
+ from mcp_dfir.config import config
11
+ from mcp_dfir.constants import VERSION
12
+
13
+
14
+ def main():
15
+ parser = argparse.ArgumentParser(prog="mcp-dfir", description="MCP Server for Performing Memory and Disk Forensics")
16
+ parser.add_argument("--version", action="version", version=f"mcp-dfir v{VERSION}")
17
+ parser.add_argument(
18
+ "-d", "--case-dir", type=Path, default=Path.cwd(), help=f"Working directory for the case, default: {Path.cwd()}"
19
+ )
20
+ parser.add_argument("--init", action="store_true", help="Create case directories then exit")
21
+ parser.add_argument(
22
+ "--clear-case-dir", action="store_true", help="Clear the case directory before starting the server"
23
+ )
24
+ parser.add_argument(
25
+ "--clear-docker", action="store_true", help="Clear the forensics docker container before starting the server"
26
+ )
27
+ args = parser.parse_args()
28
+
29
+ if args.clear_case_dir:
30
+ user_input = input(
31
+ f"Are you sure you want to clear the case directory {args.case_dir}? This action cannot be undone. Type 'yes' to confirm: "
32
+ )
33
+ if user_input != "yes":
34
+ parser.exit(status=1, message="Case directory not cleared")
35
+ for path in "artifacts", "analysis", "evidence", "symbols":
36
+ shutil.rmtree(args.case_dir / path, ignore_errors=True)
37
+ print("Case directory cleared")
38
+ sys.exit(0)
39
+
40
+ # Load config
41
+ config.load(working_directory=args.case_dir)
42
+
43
+ if args.init:
44
+ sys.exit(0)
45
+
46
+ if args.clear_docker:
47
+ from mcp_dfir.docker_manager import docker_manager
48
+
49
+ docker_manager.clear_forensic_container()
50
+ sys.exit(0)
51
+
52
+ # Load MCP server
53
+ from mcp_dfir.docker_manager import docker_manager
54
+ from mcp_dfir.server import mcp
55
+
56
+ # Start the mcp server
57
+ docker_manager.start_forensic_container()
58
+ try:
59
+ mcp.run(transport="stdio")
60
+ except KeyboardInterrupt:
61
+ pass
62
+ finally:
63
+ docker_manager.stop_forensic_container()
64
+
65
+
66
+ if __name__ == "__main__":
67
+ main()
@@ -0,0 +1,75 @@
1
+ """Defines the MCP server"""
2
+
3
+ # Third-party libraries
4
+ from mcp.server.fastmcp import FastMCP
5
+
6
+ # Project libraries
7
+ from mcp_dfir.tools.file_search import list_files, run_find, run_grep, run_strings
8
+ from mcp_dfir.tools.file_tools import untar_file, unzip_file
9
+ from mcp_dfir.tools.hash import get_hash
10
+ from mcp_dfir.tools.history import get_command_history, get_command_result
11
+ from mcp_dfir.tools.notes import (
12
+ add_analyst_note,
13
+ show_analyst_note,
14
+ show_analyst_notes_summary,
15
+ update_analyst_note,
16
+ )
17
+ from mcp_dfir.tools.sleuthkit import run_sleuthkit_command
18
+ from mcp_dfir.tools.volatility import (
19
+ download_linux_symbol,
20
+ download_windows_symbol,
21
+ run_volatility_command,
22
+ search_linux_symbols,
23
+ )
24
+
25
+ # Create MCP server
26
+ mcp = FastMCP(
27
+ "llm-forensics",
28
+ auth=None,
29
+ instructions="""\
30
+ You are a host forensics agent tasked with performing memory, disk, and artifact forensics.
31
+
32
+ Before beginning analysis always perform the following tasks:
33
+ 1. Perform an inventory of all evidence, ask the user for specific details on how, when, and where the evidence was acquired
34
+ 2. Review all previous commands that have already been run, commands should never be re-run unless the evidence has been updated or the commands were run incorrectly
35
+ 3. Review all analyst notes to not perform duplicate analysis
36
+
37
+ While performing analysis follow these rules:
38
+ 1. Create an analyst note (markdown format) after every finding whether benign, malicious, or if additional research is needed
39
+ 2. Never make any assumptions, if a conclusion cannot be reached on whether a finding is benign or malicious explicitly mark it as needing additional research
40
+ 3. Never use custom bash or python scripts to parse files, only use the built-in head/tail/regex/truncate options when going through command results
41
+ 4. All artifacts should be saved under /artifacts
42
+ """,
43
+ )
44
+
45
+ # File search
46
+ mcp.add_tool(list_files)
47
+ mcp.add_tool(run_strings)
48
+ mcp.add_tool(run_grep)
49
+ mcp.add_tool(run_find)
50
+
51
+ # File tools
52
+ mcp.add_tool(unzip_file)
53
+ mcp.add_tool(untar_file)
54
+
55
+ # Hash
56
+ mcp.add_tool(get_hash)
57
+
58
+ # Records
59
+ mcp.add_tool(get_command_history)
60
+ mcp.add_tool(get_command_result)
61
+
62
+ # Volatility
63
+ mcp.add_tool(run_volatility_command)
64
+ mcp.add_tool(download_windows_symbol)
65
+ mcp.add_tool(download_linux_symbol)
66
+ mcp.add_tool(search_linux_symbols)
67
+
68
+ # Sleuthkit
69
+ mcp.add_tool(run_sleuthkit_command)
70
+
71
+ # Notes
72
+ mcp.add_tool(show_analyst_notes_summary)
73
+ mcp.add_tool(show_analyst_note)
74
+ mcp.add_tool(add_analyst_note)
75
+ mcp.add_tool(update_analyst_note)
File without changes
@@ -0,0 +1,59 @@
1
+ """File search mcp functions"""
2
+
3
+ # Standard libraries
4
+ from pathlib import Path
5
+ from typing import Literal
6
+
7
+ # Third-party libraries
8
+ from mcp.server.fastmcp import Context
9
+
10
+ # Project libraries
11
+ from mcp_dfir.config import config
12
+ from mcp_dfir.docker_manager import docker_manager
13
+ from mcp_dfir.tools.models import CommandRecordSummary, FileDetails
14
+
15
+
16
+ def list_files(path: Literal["artifacts", "symbols", "evidence"]) -> list[FileDetails]:
17
+ valid_paths = [volume.name for volume in config.docker_volumes]
18
+
19
+ # Select volume
20
+ selected_volume = None
21
+ for volume in config.docker_volumes:
22
+ if volume.name == path:
23
+ selected_volume = volume
24
+ break
25
+ if selected_volume is None:
26
+ raise RuntimeError(f"Invalid path {path}, valid paths are {valid_paths}")
27
+
28
+ out_list = []
29
+ for dir_path, _, file_name_list in selected_volume.external_path.walk():
30
+ for file_name in file_name_list:
31
+ external_file_path = Path(dir_path / file_name)
32
+ internal_file_path = (
33
+ f"/{path}/{str(external_file_path.relative_to(selected_volume.external_path)).replace('\\', '/')}"
34
+ )
35
+ out_list.append(
36
+ FileDetails(
37
+ name=file_name,
38
+ path=internal_file_path,
39
+ size=external_file_path.stat().st_size,
40
+ )
41
+ )
42
+ return out_list
43
+
44
+
45
+ def run_strings(ctx: Context, arguments: list[str]) -> CommandRecordSummary:
46
+ """Runs: strings <arguments>"""
47
+ return docker_manager.exec_stream(ctx=ctx, command_list=["strings", *arguments])
48
+
49
+
50
+ def run_grep(ctx: Context, arguments: list[str]) -> CommandRecordSummary:
51
+ """Runs: grep <arguments>"""
52
+ return docker_manager.exec_stream(ctx=ctx, command_list=["grep", *arguments])
53
+
54
+
55
+ def run_find(ctx: Context, arguments: list[str]) -> CommandRecordSummary:
56
+ """Runs: find <arguments>"""
57
+ if " ".join(arguments).find("-exec") != -1:
58
+ raise RuntimeError("Cannot run exec in find")
59
+ return docker_manager.exec_stream(ctx=ctx, command_list=["find", *arguments])