iflow-mcp_developermode-korea_reversecore-mcp 1.0.0__py3-none-any.whl

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.
Files changed (79) hide show
  1. iflow_mcp_developermode_korea_reversecore_mcp-1.0.0.dist-info/METADATA +543 -0
  2. iflow_mcp_developermode_korea_reversecore_mcp-1.0.0.dist-info/RECORD +79 -0
  3. iflow_mcp_developermode_korea_reversecore_mcp-1.0.0.dist-info/WHEEL +5 -0
  4. iflow_mcp_developermode_korea_reversecore_mcp-1.0.0.dist-info/entry_points.txt +2 -0
  5. iflow_mcp_developermode_korea_reversecore_mcp-1.0.0.dist-info/licenses/LICENSE +21 -0
  6. iflow_mcp_developermode_korea_reversecore_mcp-1.0.0.dist-info/top_level.txt +1 -0
  7. reversecore_mcp/__init__.py +9 -0
  8. reversecore_mcp/core/__init__.py +78 -0
  9. reversecore_mcp/core/audit.py +101 -0
  10. reversecore_mcp/core/binary_cache.py +138 -0
  11. reversecore_mcp/core/command_spec.py +357 -0
  12. reversecore_mcp/core/config.py +432 -0
  13. reversecore_mcp/core/container.py +288 -0
  14. reversecore_mcp/core/decorators.py +152 -0
  15. reversecore_mcp/core/error_formatting.py +93 -0
  16. reversecore_mcp/core/error_handling.py +142 -0
  17. reversecore_mcp/core/evidence.py +229 -0
  18. reversecore_mcp/core/exceptions.py +296 -0
  19. reversecore_mcp/core/execution.py +240 -0
  20. reversecore_mcp/core/ghidra.py +642 -0
  21. reversecore_mcp/core/ghidra_helper.py +481 -0
  22. reversecore_mcp/core/ghidra_manager.py +234 -0
  23. reversecore_mcp/core/json_utils.py +131 -0
  24. reversecore_mcp/core/loader.py +73 -0
  25. reversecore_mcp/core/logging_config.py +206 -0
  26. reversecore_mcp/core/memory.py +721 -0
  27. reversecore_mcp/core/metrics.py +198 -0
  28. reversecore_mcp/core/mitre_mapper.py +365 -0
  29. reversecore_mcp/core/plugin.py +45 -0
  30. reversecore_mcp/core/r2_helpers.py +404 -0
  31. reversecore_mcp/core/r2_pool.py +403 -0
  32. reversecore_mcp/core/report_generator.py +268 -0
  33. reversecore_mcp/core/resilience.py +252 -0
  34. reversecore_mcp/core/resource_manager.py +169 -0
  35. reversecore_mcp/core/result.py +132 -0
  36. reversecore_mcp/core/security.py +213 -0
  37. reversecore_mcp/core/validators.py +238 -0
  38. reversecore_mcp/dashboard/__init__.py +221 -0
  39. reversecore_mcp/prompts/__init__.py +56 -0
  40. reversecore_mcp/prompts/common.py +24 -0
  41. reversecore_mcp/prompts/game.py +280 -0
  42. reversecore_mcp/prompts/malware.py +1219 -0
  43. reversecore_mcp/prompts/report.py +150 -0
  44. reversecore_mcp/prompts/security.py +136 -0
  45. reversecore_mcp/resources.py +329 -0
  46. reversecore_mcp/server.py +727 -0
  47. reversecore_mcp/tools/__init__.py +49 -0
  48. reversecore_mcp/tools/analysis/__init__.py +74 -0
  49. reversecore_mcp/tools/analysis/capa_tools.py +215 -0
  50. reversecore_mcp/tools/analysis/die_tools.py +180 -0
  51. reversecore_mcp/tools/analysis/diff_tools.py +643 -0
  52. reversecore_mcp/tools/analysis/lief_tools.py +272 -0
  53. reversecore_mcp/tools/analysis/signature_tools.py +591 -0
  54. reversecore_mcp/tools/analysis/static_analysis.py +479 -0
  55. reversecore_mcp/tools/common/__init__.py +58 -0
  56. reversecore_mcp/tools/common/file_operations.py +352 -0
  57. reversecore_mcp/tools/common/memory_tools.py +516 -0
  58. reversecore_mcp/tools/common/patch_explainer.py +230 -0
  59. reversecore_mcp/tools/common/server_tools.py +115 -0
  60. reversecore_mcp/tools/ghidra/__init__.py +19 -0
  61. reversecore_mcp/tools/ghidra/decompilation.py +975 -0
  62. reversecore_mcp/tools/ghidra/ghidra_tools.py +1052 -0
  63. reversecore_mcp/tools/malware/__init__.py +61 -0
  64. reversecore_mcp/tools/malware/adaptive_vaccine.py +579 -0
  65. reversecore_mcp/tools/malware/dormant_detector.py +756 -0
  66. reversecore_mcp/tools/malware/ioc_tools.py +228 -0
  67. reversecore_mcp/tools/malware/vulnerability_hunter.py +519 -0
  68. reversecore_mcp/tools/malware/yara_tools.py +214 -0
  69. reversecore_mcp/tools/patch_explainer.py +19 -0
  70. reversecore_mcp/tools/radare2/__init__.py +13 -0
  71. reversecore_mcp/tools/radare2/r2_analysis.py +972 -0
  72. reversecore_mcp/tools/radare2/r2_session.py +376 -0
  73. reversecore_mcp/tools/radare2/radare2_mcp_tools.py +1183 -0
  74. reversecore_mcp/tools/report/__init__.py +4 -0
  75. reversecore_mcp/tools/report/email.py +82 -0
  76. reversecore_mcp/tools/report/report_mcp_tools.py +344 -0
  77. reversecore_mcp/tools/report/report_tools.py +1076 -0
  78. reversecore_mcp/tools/report/session.py +194 -0
  79. reversecore_mcp/tools/report_tools.py +11 -0
@@ -0,0 +1,1183 @@
1
+ """
2
+ Radare2 MCP Tools - Direct port from r2mcp C implementation.
3
+
4
+ This module provides MCP-compatible tools that mirror the official r2mcp server,
5
+ enabling full radare2 functionality through the MCP protocol.
6
+
7
+ All tools are prefixed with 'Radare2_' for namespace clarity.
8
+
9
+ SECURITY PHILOSOPHY:
10
+ - All user inputs are strictly validated before passing to r2pipe
11
+ - Address/expression parameters are sanitized to prevent command injection
12
+ - Path validation uses the project's security module
13
+ - No shell=True, no f-strings with unsanitized input in commands
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import asyncio
19
+ import os
20
+ import shutil
21
+ from typing import Any
22
+
23
+ from fastmcp import FastMCP
24
+
25
+ from reversecore_mcp.core.config import get_config
26
+ from reversecore_mcp.core.exceptions import ValidationError
27
+ from reversecore_mcp.core.logging_config import get_logger
28
+ from reversecore_mcp.core.plugin import Plugin
29
+ from reversecore_mcp.core.security import validate_file_path
30
+ from reversecore_mcp.core.validators import validate_address_format
31
+
32
+ # Import session management and utilities from r2_session module
33
+ from reversecore_mcp.tools.radare2.r2_session import (
34
+ DEFAULT_PAGE_SIZE,
35
+ MAX_PAGE_SIZE,
36
+ R2Session,
37
+ _filter_lines_by_regex,
38
+ _filter_named_functions,
39
+ _paginate_text,
40
+ _sanitize_for_r2_cmd,
41
+ _validate_expression,
42
+ _validate_identifier,
43
+ _validate_r2_command,
44
+ )
45
+
46
+ logger = get_logger(__name__)
47
+
48
+ # Default configuration
49
+ DEFAULT_TIMEOUT = get_config().default_tool_timeout
50
+
51
+
52
+ class Radare2ToolsPlugin(Plugin):
53
+ """Plugin for Radare2 MCP tools - port from r2mcp."""
54
+
55
+ name = "radare2_mcp_tools"
56
+ description = "Radare2 binary analysis tools (r2mcp compatible)"
57
+
58
+ def __init__(self):
59
+ self._sessions: dict[str, R2Session] = {} # session_id -> Session
60
+ self._file_to_session: dict[str, str] = {} # file_path -> session_id
61
+ self._lock = asyncio.Lock() # Protects session creation race conditions
62
+
63
+ def _diagnose_error(self, file_path: str, error: Exception) -> dict[str, Any]:
64
+ """Diagnose why r2 failed to open a file."""
65
+ diagnosis = {
66
+ "error": str(error),
67
+ "file_exists": os.path.exists(file_path),
68
+ "is_file": os.path.isfile(file_path) if os.path.exists(file_path) else False,
69
+ "permissions": oct(os.stat(file_path).st_mode)[-3:]
70
+ if os.path.exists(file_path)
71
+ else "N/A",
72
+ "file_size": os.path.getsize(file_path) if os.path.exists(file_path) else 0,
73
+ "r2_available": shutil.which("radare2") is not None,
74
+ "hints": [],
75
+ }
76
+
77
+ if not diagnosis["file_exists"]:
78
+ diagnosis["hints"].append(
79
+ "Check if the file path is correct (relative to /app/workspace?)"
80
+ )
81
+ elif not diagnosis["is_file"]:
82
+ diagnosis["hints"].append("Path exists but is not a file (directory?)")
83
+ elif diagnosis["file_size"] == 0:
84
+ diagnosis["hints"].append("File is empty (0 bytes)")
85
+
86
+ return diagnosis
87
+
88
+ async def _get_or_create_session(self, file_path: str, auto_analyze: bool = False) -> R2Session:
89
+ """
90
+ Get existing session or create new one with strict validation.
91
+ Protected by lock to prevent race conditions.
92
+ """
93
+ # 1. Normalize Path
94
+ try:
95
+ validated_path = validate_file_path(file_path)
96
+ file_path = str(validated_path)
97
+ except ValidationError:
98
+ return R2Session(file_path)
99
+
100
+ async with self._lock:
101
+ # 2. Check existing session (double-checked locking pattern)
102
+ if file_path in self._file_to_session:
103
+ sid = self._file_to_session[file_path]
104
+ if sid in self._sessions:
105
+ session = self._sessions[sid]
106
+ if session.is_open:
107
+ return session
108
+ else:
109
+ # Stale session, remove it
110
+ del self._sessions[sid]
111
+ del self._file_to_session[file_path]
112
+
113
+ # 3. Create new session (blocking I/O wrapped in thread)
114
+ try:
115
+ # Validate file availability again inside lock
116
+ if not os.path.exists(file_path):
117
+ raise ValueError(f"File not found: {file_path}")
118
+
119
+ # Use to_thread for blocking R2 spawning
120
+ session = await asyncio.to_thread(R2Session, file_path)
121
+ # IMPORTANT: R2Session(file_path) does NOT open the file automatically.
122
+ # We must explicitly open it, otherwise all Radare2_* tools will fail
123
+ # with is_open == False (and Radare2_open_file always returns R2_OPEN_FAILED).
124
+ opened = await asyncio.to_thread(session.open, file_path)
125
+ if not opened:
126
+ raise ValueError(session.last_error or "Failed to open file with r2pipe")
127
+
128
+ # 4. Store session
129
+ self._sessions[session.session_id] = session
130
+ self._file_to_session[file_path] = session.session_id
131
+
132
+ # 5. Auto analyze if requested
133
+ if auto_analyze:
134
+ # Async analysis call (assuming session.analyze is async or needs wrapping)
135
+ # For now, R2Session methods are sync, so we wrap them
136
+ await asyncio.to_thread(session.cmd, "aaa")
137
+
138
+ return session
139
+
140
+ except Exception as e:
141
+ logger.error(f"Failed to create R2 session for {file_path}: {e}")
142
+ # Raise exception instead of returning dummy session that may also fail
143
+ from reversecore_mcp.core.exceptions import ToolExecutionError
144
+
145
+ raise ToolExecutionError(f"Cannot open file with radare2: {file_path}") from e
146
+
147
+ def _ensure_analyzed(self, session: R2Session, level: int = 1) -> None:
148
+ """
149
+ Ensure session has been analyzed at least once.
150
+
151
+ Args:
152
+ session: R2Session to check
153
+ level: Minimum analysis level required
154
+ """
155
+ if not session._analyzed:
156
+ session.analyze(level)
157
+
158
+ def register(self, mcp: FastMCP) -> None:
159
+ """Register all Radare2 tools with the MCP server."""
160
+
161
+ # =====================================================================
162
+ # File Management Tools
163
+ # =====================================================================
164
+
165
+ @mcp.tool()
166
+ async def Radare2_open_file(file_path: str) -> dict[str, Any]:
167
+ """
168
+ Opens a binary file with radare2 for analysis.
169
+
170
+ Call this tool before any other r2mcp tool. Use an absolute file_path.
171
+
172
+ Args:
173
+ file_path: Absolute path to the binary file to analyze
174
+
175
+ Returns:
176
+ Status of the file opening operation, including session_id
177
+ """
178
+ # Validate path using project security module
179
+ try:
180
+ validated_path = validate_file_path(file_path)
181
+ abs_path = str(validated_path)
182
+ except ValidationError as e:
183
+ return {"status": "error", "message": str(e), "error_code": "INVALID_PATH"}
184
+
185
+ session = await self._get_or_create_session(abs_path)
186
+
187
+ if session.is_open:
188
+ return {
189
+ "status": "success",
190
+ "message": "File opened successfully",
191
+ "file_path": abs_path,
192
+ "session_id": session.session_id,
193
+ "file_size": os.path.getsize(abs_path) if os.path.exists(abs_path) else 0,
194
+ "status_code": "OPENED",
195
+ }
196
+
197
+ # Diagnose failure
198
+ diagnosis = self._diagnose_error(
199
+ abs_path, Exception(session.last_error or "Unknown error")
200
+ )
201
+ return {
202
+ "status": "error",
203
+ "message": f"Failed to open file: {session.last_error}",
204
+ "error_code": "R2_OPEN_FAILED",
205
+ "diagnosis": diagnosis,
206
+ "hints": diagnosis["hints"],
207
+ "attempts": 1,
208
+ }
209
+
210
+ @mcp.tool()
211
+ async def Radare2_close_file(file_path: str) -> dict[str, Any]:
212
+ """
213
+ Close the currently open radare2 session for a file.
214
+
215
+ Args:
216
+ file_path: Path to the file to close
217
+
218
+ Returns:
219
+ Status of the close operation
220
+ """
221
+ try:
222
+ validated_path = validate_file_path(file_path)
223
+ abs_path = str(validated_path)
224
+
225
+ # Check mapping
226
+ if abs_path in self._file_to_session:
227
+ sid = self._file_to_session[abs_path]
228
+ if sid in self._sessions:
229
+ self._sessions[sid].close()
230
+ del self._sessions[sid]
231
+ del self._file_to_session[abs_path]
232
+ return {
233
+ "status": "success",
234
+ "message": "File closed successfully",
235
+ "session_id": sid,
236
+ }
237
+
238
+ return {"status": "success", "message": "File was not open (no active session)"}
239
+ except ValidationError as e:
240
+ return {"status": "error", "message": str(e)}
241
+
242
+ # =====================================================================
243
+ # Analysis Tools
244
+ # =====================================================================
245
+
246
+ @mcp.tool()
247
+ async def Radare2_analyze(
248
+ file_path: str,
249
+ level: int = 2,
250
+ ) -> dict[str, Any]:
251
+ """
252
+ Run binary analysis with optional depth level.
253
+
254
+ Args:
255
+ file_path: Path to the binary file
256
+ level: Analysis level 0-4 (higher = more thorough, slower)
257
+ 0: Basic (aa)
258
+ 1: Auto (aaa)
259
+ 2: Experimental (aaaa) - default
260
+ 3: Deep (aaaaa)
261
+ 4: Very deep (aaaaaa)
262
+
263
+ Returns:
264
+ Analysis result with function count
265
+ """
266
+ # Validate level is in range
267
+ if not isinstance(level, int) or level < 0 or level > 4:
268
+ return {"status": "error", "message": "level must be 0-4"}
269
+
270
+ session = await self._get_or_create_session(file_path)
271
+ if not session.is_open:
272
+ return {"status": "error", "message": "Failed to open file"}
273
+
274
+ session.analyze(level)
275
+ func_count = session.cmd("aflc").strip()
276
+
277
+ return {
278
+ "status": "success",
279
+ "message": f"Analysis completed with level {level}",
280
+ "function_count": int(func_count) if func_count.isdigit() else 0,
281
+ }
282
+
283
+ @mcp.tool()
284
+ async def Radare2_run_command(
285
+ file_path: str,
286
+ command: str,
287
+ ) -> dict[str, Any]:
288
+ """
289
+ Execute a raw radare2 command directly.
290
+
291
+ NOTE: Only analysis commands are allowed. Write and shell commands are blocked.
292
+
293
+ Args:
294
+ file_path: Path to the binary file
295
+ command: The radare2 command to execute
296
+
297
+ Returns:
298
+ Command output
299
+ """
300
+ # Validate command for security
301
+ try:
302
+ _validate_r2_command(command)
303
+ except ValidationError as e:
304
+ return {"status": "error", "message": str(e)}
305
+
306
+ session = await self._get_or_create_session(file_path)
307
+ if not session.is_open:
308
+ return {"status": "error", "message": "Failed to open file"}
309
+
310
+ result = session.cmd(command)
311
+ return {"status": "success", "output": result}
312
+
313
+ @mcp.tool()
314
+ async def Radare2_calculate(
315
+ file_path: str,
316
+ expression: str,
317
+ ) -> dict[str, Any]:
318
+ """
319
+ Evaluate a math expression using radare2's number parser.
320
+
321
+ Useful for: 64-bit math, resolving addresses for symbols,
322
+ avoiding hallucinated results.
323
+
324
+ Args:
325
+ file_path: Path to the binary file
326
+ expression: Math expression to evaluate (e.g., "0x100 + sym.flag - 4")
327
+
328
+ Returns:
329
+ Calculated result in hex and decimal
330
+ """
331
+ # Validate expression for security
332
+ try:
333
+ _validate_expression(expression)
334
+ except ValidationError as e:
335
+ return {"status": "error", "message": str(e)}
336
+
337
+ session = await self._get_or_create_session(file_path)
338
+ if not session.is_open:
339
+ return {"status": "error", "message": "Failed to open file"}
340
+
341
+ # Use validated expression
342
+ result = session.cmd(f"?v {expression}").strip()
343
+ return {
344
+ "status": "success",
345
+ "result": result,
346
+ "expression": expression,
347
+ }
348
+
349
+ # =====================================================================
350
+ # Function Listing Tools
351
+ # =====================================================================
352
+
353
+ @mcp.tool()
354
+ async def Radare2_list_functions(
355
+ file_path: str,
356
+ only_named: bool = False,
357
+ filter: str | None = None,
358
+ ) -> dict[str, Any]:
359
+ """
360
+ List all functions discovered during analysis.
361
+
362
+ Args:
363
+ file_path: Path to the binary file
364
+ only_named: If true, exclude functions with numeric suffixes
365
+ filter: Regular expression to filter results
366
+
367
+ Returns:
368
+ List of functions with addresses and names
369
+ """
370
+ session = await self._get_or_create_session(file_path)
371
+ if not session.is_open:
372
+ return {"status": "error", "message": "Failed to open file"}
373
+
374
+ # Ensure analysis is done (lazy - only if not already analyzed)
375
+ self._ensure_analyzed(session)
376
+ result = session.cmd("afl")
377
+
378
+ if only_named:
379
+ result = _filter_named_functions(result)
380
+
381
+ if filter:
382
+ result = _filter_lines_by_regex(result, filter)
383
+
384
+ lines = [line for line in result.strip().split("\n") if line]
385
+ return {
386
+ "status": "success",
387
+ "count": len(lines),
388
+ "functions": result,
389
+ }
390
+
391
+ @mcp.tool()
392
+ async def Radare2_list_functions_tree(
393
+ file_path: str,
394
+ ) -> dict[str, Any]:
395
+ """
396
+ List functions and their successors (call tree).
397
+
398
+ Args:
399
+ file_path: Path to the binary file
400
+
401
+ Returns:
402
+ Function call tree
403
+ """
404
+ session = await self._get_or_create_session(file_path)
405
+ if not session.is_open:
406
+ return {"status": "error", "message": "Failed to open file"}
407
+
408
+ result = session.cmd("aflm")
409
+ return {"status": "success", "output": result.strip()}
410
+
411
+ @mcp.tool()
412
+ async def Radare2_show_function_details(
413
+ file_path: str,
414
+ address: str | None = None,
415
+ ) -> dict[str, Any]:
416
+ """
417
+ Display detailed information about a function.
418
+
419
+ Args:
420
+ file_path: Path to the binary file
421
+ address: Function address (uses current if not specified)
422
+
423
+ Returns:
424
+ Detailed function information
425
+ """
426
+ session = await self._get_or_create_session(file_path)
427
+ if not session.is_open:
428
+ return {"status": "error", "message": "Failed to open file"}
429
+
430
+ if address:
431
+ # Validate address format
432
+ try:
433
+ validate_address_format(address)
434
+ except ValidationError as e:
435
+ return {"status": "error", "message": str(e)}
436
+ result = session.cmd(f"afi @ {address}")
437
+ else:
438
+ result = session.cmd("afi")
439
+
440
+ return {"status": "success", "output": result}
441
+
442
+ @mcp.tool()
443
+ async def Radare2_get_current_address(
444
+ file_path: str,
445
+ ) -> dict[str, Any]:
446
+ """
447
+ Show the current seek position and function name.
448
+
449
+ Args:
450
+ file_path: Path to the binary file
451
+
452
+ Returns:
453
+ Current address and function name
454
+ """
455
+ session = await self._get_or_create_session(file_path)
456
+ if not session.is_open:
457
+ return {"status": "error", "message": "Failed to open file"}
458
+
459
+ address = session.cmd("s").strip()
460
+ func_name = session.cmd("fd").strip()
461
+
462
+ return {
463
+ "status": "success",
464
+ "address": address,
465
+ "function": func_name,
466
+ }
467
+
468
+ @mcp.tool()
469
+ async def Radare2_get_function_prototype(
470
+ file_path: str,
471
+ address: str,
472
+ ) -> dict[str, Any]:
473
+ """
474
+ Retrieve the function signature at the specified address.
475
+
476
+ Args:
477
+ file_path: Path to the binary file
478
+ address: Address of the function
479
+
480
+ Returns:
481
+ Function prototype/signature
482
+ """
483
+ # Validate address
484
+ try:
485
+ validate_address_format(address)
486
+ except ValidationError as e:
487
+ return {"status": "error", "message": str(e)}
488
+
489
+ session = await self._get_or_create_session(file_path)
490
+ if not session.is_open:
491
+ return {"status": "error", "message": "Failed to open file"}
492
+
493
+ result = session.cmd(f"afs @ {address}").strip()
494
+ return {"status": "success", "prototype": result}
495
+
496
+ @mcp.tool()
497
+ async def Radare2_set_function_prototype(
498
+ file_path: str,
499
+ address: str,
500
+ prototype: str,
501
+ ) -> dict[str, Any]:
502
+ """
503
+ Set the function signature (return type, name, arguments).
504
+
505
+ Args:
506
+ file_path: Path to the binary file
507
+ address: Address of the function
508
+ prototype: Function signature in C-like syntax
509
+
510
+ Returns:
511
+ Confirmation
512
+ """
513
+ # Validate address
514
+ try:
515
+ validate_address_format(address)
516
+ except ValidationError as e:
517
+ return {"status": "error", "message": str(e)}
518
+
519
+ # Sanitize prototype (remove dangerous chars)
520
+ safe_prototype = _sanitize_for_r2_cmd(prototype)
521
+ if not safe_prototype:
522
+ return {"status": "error", "message": "Invalid prototype"}
523
+
524
+ session = await self._get_or_create_session(file_path)
525
+ if not session.is_open:
526
+ return {"status": "error", "message": "Failed to open file"}
527
+
528
+ session.cmd(f"afs {safe_prototype} @ {address}")
529
+ return {"status": "success", "message": "Function prototype set"}
530
+
531
+ # =====================================================================
532
+ # Binary Information Tools
533
+ # =====================================================================
534
+
535
+ @mcp.tool()
536
+ async def Radare2_show_headers(
537
+ file_path: str,
538
+ ) -> dict[str, Any]:
539
+ """
540
+ Display binary headers and file information.
541
+
542
+ Args:
543
+ file_path: Path to the binary file
544
+
545
+ Returns:
546
+ Binary header information
547
+ """
548
+ session = await self._get_or_create_session(file_path)
549
+ if not session.is_open:
550
+ return {"status": "error", "message": "Failed to open file"}
551
+
552
+ info = session.cmd("i")
553
+ headers = session.cmd("iH")
554
+
555
+ return {
556
+ "status": "success",
557
+ "info": info,
558
+ "headers": headers,
559
+ }
560
+
561
+ @mcp.tool()
562
+ async def Radare2_list_sections(
563
+ file_path: str,
564
+ ) -> dict[str, Any]:
565
+ """
566
+ Display memory sections and segments from the binary.
567
+
568
+ Args:
569
+ file_path: Path to the binary file
570
+
571
+ Returns:
572
+ Sections and segments information
573
+ """
574
+ session = await self._get_or_create_session(file_path)
575
+ if not session.is_open:
576
+ return {"status": "error", "message": "Failed to open file"}
577
+
578
+ sections = session.cmd("iS")
579
+ segments = session.cmd("iSS")
580
+
581
+ return {
582
+ "status": "success",
583
+ "sections": sections,
584
+ "segments": segments,
585
+ }
586
+
587
+ @mcp.tool()
588
+ async def Radare2_list_imports(
589
+ file_path: str,
590
+ filter: str | None = None,
591
+ ) -> dict[str, Any]:
592
+ """
593
+ List imported symbols.
594
+
595
+ Note: Use list_symbols for addresses with sym.imp. prefix.
596
+
597
+ Args:
598
+ file_path: Path to the binary file
599
+ filter: Regular expression to filter results
600
+
601
+ Returns:
602
+ List of imports
603
+ """
604
+ session = await self._get_or_create_session(file_path)
605
+ if not session.is_open:
606
+ return {"status": "error", "message": "Failed to open file"}
607
+
608
+ result = session.cmd("ii")
609
+
610
+ if filter:
611
+ result = _filter_lines_by_regex(result, filter)
612
+
613
+ return {"status": "success", "imports": result}
614
+
615
+ @mcp.tool()
616
+ async def Radare2_list_symbols(
617
+ file_path: str,
618
+ filter: str | None = None,
619
+ ) -> dict[str, Any]:
620
+ """
621
+ Show all symbols (functions, variables, imports) with addresses.
622
+
623
+ Args:
624
+ file_path: Path to the binary file
625
+ filter: Regular expression to filter results
626
+
627
+ Returns:
628
+ List of symbols
629
+ """
630
+ session = await self._get_or_create_session(file_path)
631
+ if not session.is_open:
632
+ return {"status": "error", "message": "Failed to open file"}
633
+
634
+ result = session.cmd("is")
635
+
636
+ if filter:
637
+ result = _filter_lines_by_regex(result, filter)
638
+
639
+ return {"status": "success", "symbols": result}
640
+
641
+ @mcp.tool()
642
+ async def Radare2_list_entrypoints(
643
+ file_path: str,
644
+ ) -> dict[str, Any]:
645
+ """
646
+ Display program entrypoints, constructors and main function.
647
+
648
+ Args:
649
+ file_path: Path to the binary file
650
+
651
+ Returns:
652
+ Entrypoint information
653
+ """
654
+ session = await self._get_or_create_session(file_path)
655
+ if not session.is_open:
656
+ return {"status": "error", "message": "Failed to open file"}
657
+
658
+ result = session.cmd("ie")
659
+ return {"status": "success", "entrypoints": result}
660
+
661
+ @mcp.tool()
662
+ async def Radare2_list_libraries(
663
+ file_path: str,
664
+ ) -> dict[str, Any]:
665
+ """
666
+ List all shared libraries linked to the binary.
667
+
668
+ Args:
669
+ file_path: Path to the binary file
670
+
671
+ Returns:
672
+ List of linked libraries
673
+ """
674
+ session = await self._get_or_create_session(file_path)
675
+ if not session.is_open:
676
+ return {"status": "error", "message": "Failed to open file"}
677
+
678
+ result = session.cmd("il")
679
+ return {"status": "success", "libraries": result}
680
+
681
+ @mcp.tool()
682
+ async def Radare2_list_strings(
683
+ file_path: str,
684
+ filter: str | None = None,
685
+ cursor: str | None = None,
686
+ page_size: int = DEFAULT_PAGE_SIZE,
687
+ ) -> dict[str, Any]:
688
+ """
689
+ List strings from data sections with optional regex filter.
690
+
691
+ Args:
692
+ file_path: Path to the binary file
693
+ filter: Regular expression to filter results
694
+ cursor: Pagination cursor (line number to start from)
695
+ page_size: Number of lines per page (default: 1000, max: 10000)
696
+
697
+ Returns:
698
+ List of strings with pagination
699
+ """
700
+ session = await self._get_or_create_session(file_path)
701
+ if not session.is_open:
702
+ return {"status": "error", "message": "Failed to open file"}
703
+
704
+ if page_size > MAX_PAGE_SIZE:
705
+ page_size = MAX_PAGE_SIZE
706
+
707
+ result = session.cmd("iz")
708
+
709
+ if filter:
710
+ result = _filter_lines_by_regex(result, filter)
711
+
712
+ paginated, has_more, next_cursor = _paginate_text(result, cursor, page_size)
713
+
714
+ return {
715
+ "status": "success",
716
+ "strings": paginated,
717
+ "has_more": has_more,
718
+ "next_cursor": next_cursor,
719
+ }
720
+
721
+ @mcp.tool()
722
+ async def Radare2_list_all_strings(
723
+ file_path: str,
724
+ filter: str | None = None,
725
+ cursor: str | None = None,
726
+ page_size: int = DEFAULT_PAGE_SIZE,
727
+ ) -> dict[str, Any]:
728
+ """
729
+ Scan the entire binary for strings with optional regex filter.
730
+
731
+ More thorough than list_strings, but slower.
732
+
733
+ Args:
734
+ file_path: Path to the binary file
735
+ filter: Regular expression to filter results
736
+ cursor: Pagination cursor
737
+ page_size: Number of lines per page
738
+
739
+ Returns:
740
+ List of all strings with pagination
741
+ """
742
+ session = await self._get_or_create_session(file_path)
743
+ if not session.is_open:
744
+ return {"status": "error", "message": "Failed to open file"}
745
+
746
+ if page_size > MAX_PAGE_SIZE:
747
+ page_size = MAX_PAGE_SIZE
748
+
749
+ result = session.cmd("izz")
750
+
751
+ if filter:
752
+ result = _filter_lines_by_regex(result, filter)
753
+
754
+ paginated, has_more, next_cursor = _paginate_text(result, cursor, page_size)
755
+
756
+ return {
757
+ "status": "success",
758
+ "strings": paginated,
759
+ "has_more": has_more,
760
+ "next_cursor": next_cursor,
761
+ }
762
+
763
+ # =====================================================================
764
+ # Class/OOP Tools
765
+ # =====================================================================
766
+
767
+ @mcp.tool()
768
+ async def Radare2_list_classes(
769
+ file_path: str,
770
+ filter: str | None = None,
771
+ ) -> dict[str, Any]:
772
+ """
773
+ List class names from various languages (C++, ObjC, Swift, Java, Dalvik).
774
+
775
+ Args:
776
+ file_path: Path to the binary file
777
+ filter: Regular expression to filter results
778
+
779
+ Returns:
780
+ List of classes
781
+ """
782
+ session = await self._get_or_create_session(file_path)
783
+ if not session.is_open:
784
+ return {"status": "error", "message": "Failed to open file"}
785
+
786
+ result = session.cmd("ic")
787
+
788
+ if filter:
789
+ result = _filter_lines_by_regex(result, filter)
790
+
791
+ return {"status": "success", "classes": result}
792
+
793
+ @mcp.tool()
794
+ async def Radare2_list_methods(
795
+ file_path: str,
796
+ classname: str,
797
+ ) -> dict[str, Any]:
798
+ """
799
+ List all methods belonging to the specified class.
800
+
801
+ Args:
802
+ file_path: Path to the binary file
803
+ classname: Name of the class to list methods for
804
+
805
+ Returns:
806
+ List of methods in the class
807
+ """
808
+ # Validate classname to prevent injection
809
+ try:
810
+ _validate_identifier(classname, "classname")
811
+ except ValidationError as e:
812
+ return {"status": "error", "message": str(e)}
813
+
814
+ session = await self._get_or_create_session(file_path)
815
+ if not session.is_open:
816
+ return {"status": "error", "message": "Failed to open file"}
817
+
818
+ result = session.cmd(f"ic {classname}")
819
+ return {"status": "success", "methods": result}
820
+
821
+ # =====================================================================
822
+ # Disassembly & Decompilation Tools
823
+ # =====================================================================
824
+
825
+ @mcp.tool()
826
+ async def Radare2_disassemble(
827
+ file_path: str,
828
+ address: str,
829
+ num_instructions: int = 10,
830
+ ) -> dict[str, Any]:
831
+ """
832
+ Disassemble a specific number of instructions from an address.
833
+
834
+ Use this to inspect a portion of memory as code without depending
835
+ on function analysis boundaries.
836
+
837
+ Args:
838
+ file_path: Path to the binary file
839
+ address: Address to start disassembly
840
+ num_instructions: Number of instructions to disassemble (default: 10, max: 1000)
841
+
842
+ Returns:
843
+ Disassembled instructions
844
+ """
845
+ # Validate address
846
+ try:
847
+ validate_address_format(address)
848
+ except ValidationError as e:
849
+ return {"status": "error", "message": str(e)}
850
+
851
+ # Limit instructions to prevent abuse
852
+ if not isinstance(num_instructions, int) or num_instructions < 1:
853
+ num_instructions = 10
854
+ if num_instructions > 1000:
855
+ num_instructions = 1000
856
+
857
+ session = await self._get_or_create_session(file_path)
858
+ if not session.is_open:
859
+ return {"status": "error", "message": "Failed to open file"}
860
+
861
+ result = session.cmd(f"pd {num_instructions} @ {address}")
862
+ return {"status": "success", "disassembly": result}
863
+
864
+ @mcp.tool()
865
+ async def Radare2_disassemble_function(
866
+ file_path: str,
867
+ address: str,
868
+ cursor: str | None = None,
869
+ page_size: int = DEFAULT_PAGE_SIZE,
870
+ ) -> dict[str, Any]:
871
+ """
872
+ Show assembly listing of the function at the specified address.
873
+
874
+ Args:
875
+ file_path: Path to the binary file
876
+ address: Address of the function to disassemble
877
+ cursor: Pagination cursor
878
+ page_size: Number of lines per page
879
+
880
+ Returns:
881
+ Function disassembly with pagination
882
+ """
883
+ # Validate address
884
+ try:
885
+ validate_address_format(address)
886
+ except ValidationError as e:
887
+ return {"status": "error", "message": str(e)}
888
+
889
+ session = await self._get_or_create_session(file_path)
890
+ if not session.is_open:
891
+ return {"status": "error", "message": "Failed to open file"}
892
+
893
+ if page_size > MAX_PAGE_SIZE:
894
+ page_size = MAX_PAGE_SIZE
895
+
896
+ result = session.cmd(f"pdf @ {address}")
897
+ paginated, has_more, next_cursor = _paginate_text(result, cursor, page_size)
898
+
899
+ return {
900
+ "status": "success",
901
+ "disassembly": paginated,
902
+ "has_more": has_more,
903
+ "next_cursor": next_cursor,
904
+ }
905
+
906
+ @mcp.tool()
907
+ async def Radare2_decompile_function(
908
+ file_path: str,
909
+ address: str,
910
+ cursor: str | None = None,
911
+ page_size: int = DEFAULT_PAGE_SIZE,
912
+ ) -> dict[str, Any]:
913
+ """
914
+ Show C-like pseudocode of the function at the given address.
915
+
916
+ Use this to inspect code in a function. Do not run multiple times
917
+ on the same offset.
918
+
919
+ Args:
920
+ file_path: Path to the binary file
921
+ address: Address of the function to decompile
922
+ cursor: Pagination cursor
923
+ page_size: Number of lines per page
924
+
925
+ Returns:
926
+ Decompiled pseudocode with pagination
927
+ """
928
+ # Validate address
929
+ try:
930
+ validate_address_format(address)
931
+ except ValidationError as e:
932
+ return {"status": "error", "message": str(e)}
933
+
934
+ session = await self._get_or_create_session(file_path)
935
+ if not session.is_open:
936
+ return {"status": "error", "message": "Failed to open file"}
937
+
938
+ if page_size > MAX_PAGE_SIZE:
939
+ page_size = MAX_PAGE_SIZE
940
+
941
+ result = session.cmd(f"pdc @ {address}")
942
+ paginated, has_more, next_cursor = _paginate_text(result, cursor, page_size)
943
+
944
+ return {
945
+ "status": "success",
946
+ "decompiled": paginated,
947
+ "has_more": has_more,
948
+ "next_cursor": next_cursor,
949
+ }
950
+
951
+ @mcp.tool()
952
+ async def Radare2_list_decompilers(
953
+ file_path: str,
954
+ ) -> dict[str, Any]:
955
+ """
956
+ Show all available decompiler backends.
957
+
958
+ Args:
959
+ file_path: Path to the binary file
960
+
961
+ Returns:
962
+ List of available decompilers
963
+ """
964
+ session = await self._get_or_create_session(file_path)
965
+ if not session.is_open:
966
+ return {"status": "error", "message": "Failed to open file"}
967
+
968
+ result = session.cmd("e cmd.pdc=?")
969
+ return {"status": "success", "decompilers": result}
970
+
971
+ @mcp.tool()
972
+ async def Radare2_use_decompiler(
973
+ file_path: str,
974
+ name: str,
975
+ ) -> dict[str, Any]:
976
+ """
977
+ Select which decompiler backend to use.
978
+
979
+ Args:
980
+ file_path: Path to the binary file
981
+ name: Decompiler name (ghidra, r2dec, pdc)
982
+
983
+ Returns:
984
+ Confirmation or error
985
+ """
986
+ session = await self._get_or_create_session(file_path)
987
+ if not session.is_open:
988
+ return {"status": "error", "message": "Failed to open file"}
989
+
990
+ available = session.cmd("e cmd.pdc=?")
991
+
992
+ # Whitelist of allowed decompilers
993
+ decompiler_map = {
994
+ "ghidra": "pdg",
995
+ "r2dec": "pdd",
996
+ "pdc": "pdc",
997
+ }
998
+
999
+ name_lower = name.lower()
1000
+ if name_lower not in decompiler_map:
1001
+ return {
1002
+ "status": "error",
1003
+ "message": f"Unknown decompiler: {name}. Allowed: ghidra, r2dec, pdc",
1004
+ }
1005
+
1006
+ cmd_name = decompiler_map[name_lower]
1007
+ if cmd_name not in available:
1008
+ return {"status": "error", "message": f"Decompiler {name} is not available"}
1009
+
1010
+ session.cmd(f"e cmd.pdc={cmd_name}")
1011
+ return {"status": "success", "message": f"Decompiler set to {name}"}
1012
+
1013
+ # =====================================================================
1014
+ # Cross-Reference Tools
1015
+ # =====================================================================
1016
+
1017
+ @mcp.tool()
1018
+ async def Radare2_xrefs_to(
1019
+ file_path: str,
1020
+ address: str,
1021
+ ) -> dict[str, Any]:
1022
+ """
1023
+ Find all code references TO the specified address.
1024
+
1025
+ Args:
1026
+ file_path: Path to the binary file
1027
+ address: Address to check for cross-references
1028
+
1029
+ Returns:
1030
+ List of xrefs to the address
1031
+ """
1032
+ # Validate address
1033
+ try:
1034
+ validate_address_format(address)
1035
+ except ValidationError as e:
1036
+ return {"status": "error", "message": str(e)}
1037
+
1038
+ session = await self._get_or_create_session(file_path)
1039
+ if not session.is_open:
1040
+ return {"status": "error", "message": "Failed to open file"}
1041
+
1042
+ result = session.cmd(f"axt @ {address}")
1043
+ return {"status": "success", "xrefs": result}
1044
+
1045
+ # =====================================================================
1046
+ # Modification Tools
1047
+ # =====================================================================
1048
+
1049
+ @mcp.tool()
1050
+ async def Radare2_rename_function(
1051
+ file_path: str,
1052
+ address: str,
1053
+ name: str,
1054
+ ) -> dict[str, Any]:
1055
+ """
1056
+ Rename the function at the specified address.
1057
+
1058
+ Args:
1059
+ file_path: Path to the binary file
1060
+ address: Address of the function to rename
1061
+ name: New function name
1062
+
1063
+ Returns:
1064
+ Confirmation
1065
+ """
1066
+ # Validate inputs
1067
+ try:
1068
+ validate_address_format(address)
1069
+ _validate_identifier(name, "name")
1070
+ except ValidationError as e:
1071
+ return {"status": "error", "message": str(e)}
1072
+
1073
+ session = await self._get_or_create_session(file_path)
1074
+ if not session.is_open:
1075
+ return {"status": "error", "message": "Failed to open file"}
1076
+
1077
+ session.cmd(f"afn {name} @ {address}")
1078
+ return {"status": "success", "message": f"Function renamed to {name}"}
1079
+
1080
+ @mcp.tool()
1081
+ async def Radare2_rename_flag(
1082
+ file_path: str,
1083
+ address: str,
1084
+ name: str,
1085
+ new_name: str,
1086
+ ) -> dict[str, Any]:
1087
+ """
1088
+ Rename a flag (variable or data reference) at the specified address.
1089
+
1090
+ Args:
1091
+ file_path: Path to the binary file
1092
+ address: Address of the flag
1093
+ name: Current flag name
1094
+ new_name: New flag name
1095
+
1096
+ Returns:
1097
+ Confirmation
1098
+ """
1099
+ # Validate all inputs
1100
+ try:
1101
+ validate_address_format(address)
1102
+ _validate_identifier(name, "name")
1103
+ _validate_identifier(new_name, "new_name")
1104
+ except ValidationError as e:
1105
+ return {"status": "error", "message": str(e)}
1106
+
1107
+ session = await self._get_or_create_session(file_path)
1108
+ if not session.is_open:
1109
+ return {"status": "error", "message": "Failed to open file"}
1110
+
1111
+ result = session.cmd(f"fr {name} {new_name} @ {address}")
1112
+ if result.strip():
1113
+ return {"status": "error", "message": result}
1114
+ return {"status": "success", "message": f"Flag renamed to {new_name}"}
1115
+
1116
+ @mcp.tool()
1117
+ async def Radare2_set_comment(
1118
+ file_path: str,
1119
+ address: str,
1120
+ message: str,
1121
+ ) -> dict[str, Any]:
1122
+ """
1123
+ Add a comment at the specified address.
1124
+
1125
+ Args:
1126
+ file_path: Path to the binary file
1127
+ address: Address to add comment
1128
+ message: Comment text
1129
+
1130
+ Returns:
1131
+ Confirmation
1132
+ """
1133
+ # Validate address
1134
+ try:
1135
+ validate_address_format(address)
1136
+ except ValidationError as e:
1137
+ return {"status": "error", "message": str(e)}
1138
+
1139
+ # Sanitize message (remove dangerous chars but allow more characters for comments)
1140
+ safe_message = _sanitize_for_r2_cmd(message)
1141
+ if not safe_message:
1142
+ return {"status": "error", "message": "Comment message is empty or invalid"}
1143
+
1144
+ session = await self._get_or_create_session(file_path)
1145
+ if not session.is_open:
1146
+ return {"status": "error", "message": "Failed to open file"}
1147
+
1148
+ session.cmd(f"CC {safe_message} @ {address}")
1149
+ return {"status": "success", "message": "Comment added"}
1150
+
1151
+ # NOTE: Radare2_list_files and Radare2_run_javascript are REMOVED
1152
+ # for security reasons:
1153
+ # - list_files: potential path traversal attack vector
1154
+ # - run_javascript: arbitrary code execution risk
1155
+
1156
+ # =====================================================================
1157
+ # Advanced Analysis Tools (from r2_analysis module)
1158
+ # =====================================================================
1159
+ # Import and register advanced analysis tools for unified plugin management
1160
+ from reversecore_mcp.tools.radare2.r2_analysis import (
1161
+ analyze_xrefs,
1162
+ generate_function_graph,
1163
+ run_radare2,
1164
+ trace_execution_path,
1165
+ )
1166
+
1167
+ mcp.tool(run_radare2)
1168
+ mcp.tool(trace_execution_path)
1169
+ mcp.tool(generate_function_graph)
1170
+ mcp.tool(analyze_xrefs)
1171
+
1172
+ logger.info(f"Registered {self.name} plugin with 34 Radare2 tools (security hardened)")
1173
+
1174
+
1175
+ def register_radare2_tools(mcp: FastMCP) -> None:
1176
+ """
1177
+ Register Radare2 tools with an MCP server instance.
1178
+
1179
+ Args:
1180
+ mcp: FastMCP server instance
1181
+ """
1182
+ plugin = Radare2ToolsPlugin()
1183
+ plugin.register(mcp)