unmap_stack_trace
Reverse obfuscated identifiers in a Python stack trace using a mapping.json file.
Instructions
Reverse obfuscated identifiers in a stack trace using a pyobfus mapping.json. Accepts the trace as plain text and the path to a mapping file produced by --save-mapping.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| trace | Yes | ||
| mapping_path | Yes |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
| result | Yes |
Implementation Reference
- pyobfus_mcp/pyobfus_mcp/tools.py:107-152 (handler)Main tool handler: unmap_stack_trace loads an ObfuscationMapping from a JSON file, calls mapping.unmap_text(trace) to reverse obfuscated identifiers, and returns the original trace, mapping stats, and an AI hint.
def unmap_stack_trace(trace: str, mapping_path: str) -> Dict[str, Any]: """Reverse obfuscated identifiers in a stack trace using a mapping.json. Wraps `pyobfus --unmap`. Accepts the trace as a literal string (most useful for agent workflows where the trace is already in the chat buffer); for large logs, callers can pre-read the file and pass its contents. Args: trace: Obfuscated stack trace or error log as plain text. mapping_path: Filesystem path to a mapping.json produced by `pyobfus ... --save-mapping PATH`. Returns: Dict with keys: status, original_trace, unmapped_trace, mapping_stats, ai_hint. """ try: from pyobfus.core.mapping import ObfuscationMapping except ImportError as e: return _error("PyobfusNotInstalled", str(e), "pip install pyobfus") mp = Path(mapping_path) if not mp.exists(): return _error( "MappingNotFound", f"Mapping file not found: {mapping_path}", "Generate one with: pyobfus src/ -o dist/ --save-mapping mapping.json", ) try: mapping = ObfuscationMapping.load(mp) except (ValueError, OSError) as e: return _error("InvalidMapping", str(e), "Regenerate the mapping file.") unmapped = mapping.unmap_text(trace) return { "status": "success", "original_trace": trace, "unmapped_trace": unmapped, "mapping_stats": mapping.stats(), "ai_hint": ( "Names are reversed, but line numbers still point to the obfuscated " "file. Cross-reference with the original source if needed." ), } - pyobfus_mcp/pyobfus_mcp/server.py:90-99 (registration)MCP tool registration: @app.tool(name='unmap_stack_trace', ...) wrapping the unmap_stack_trace callable from tools.py as an MCP tool.
@app.tool( name="unmap_stack_trace", description=( "Reverse obfuscated identifiers in a stack trace using a " "pyobfus mapping.json. Accepts the trace as plain text and the " "path to a mapping file produced by --save-mapping." ), ) def _unmap(trace: str, mapping_path: str) -> Dict[str, Any]: return unmap_stack_trace(trace, mapping_path) - Input/output schema: accepts trace (str) and mapping_path (str), returns Dict with status, original_trace, unmapped_trace, mapping_stats, ai_hint.
def unmap_stack_trace(trace: str, mapping_path: str) -> Dict[str, Any]: """Reverse obfuscated identifiers in a stack trace using a mapping.json. Wraps `pyobfus --unmap`. Accepts the trace as a literal string (most useful for agent workflows where the trace is already in the chat buffer); for large logs, callers can pre-read the file and pass its contents. Args: trace: Obfuscated stack trace or error log as plain text. mapping_path: Filesystem path to a mapping.json produced by `pyobfus ... --save-mapping PATH`. Returns: Dict with keys: status, original_trace, unmapped_trace, mapping_stats, ai_hint. """ try: from pyobfus.core.mapping import ObfuscationMapping except ImportError as e: return _error("PyobfusNotInstalled", str(e), "pip install pyobfus") mp = Path(mapping_path) if not mp.exists(): return _error( "MappingNotFound", f"Mapping file not found: {mapping_path}", "Generate one with: pyobfus src/ -o dist/ --save-mapping mapping.json", ) try: mapping = ObfuscationMapping.load(mp) except (ValueError, OSError) as e: return _error("InvalidMapping", str(e), "Regenerate the mapping file.") unmapped = mapping.unmap_text(trace) return { "status": "success", "original_trace": trace, "unmapped_trace": unmapped, "mapping_stats": mapping.stats(), "ai_hint": ( "Names are reversed, but line numbers still point to the obfuscated " "file. Cross-reference with the original source if needed." ), } - pyobfus/core/mapping.py:186-225 (helper)Core helper: ObfuscationMapping.reverse() looks up an obfuscated name; reverse_qualified() handles dotted segments; unmap_text() replaces all known obfuscated identifiers in text; stats() returns mapping statistics.
def reverse(self, obfuscated_name: str) -> Optional[str]: """Look up an obfuscated identifier and return the original name, or None.""" info = self.global_map.get(obfuscated_name) if info is None: return None return info[1] or None def reverse_qualified(self, qualified: str) -> str: """Unmap every dotted segment: `I0.I1` -> `MyClass.my_method`.""" parts = qualified.split(".") out: List[str] = [] for part in parts: out.append(self.reverse(part) or part) return ".".join(out) def unmap_text(self, text: str) -> str: """ Replace every occurrence of a known obfuscated identifier in `text` with its original name. Non-obfuscated tokens pass through unchanged. Substitution is token-boundary aware (uses \\b identifier matches), so e.g. "I1" inside "MyI1Thing" is NOT replaced. """ if not self.global_map: return text def _sub(match: "re.Match[str]") -> str: tok = match.group(1) original = self.reverse(tok) return original if original else tok return _IDENT_RE.sub(_sub, text) def stats(self) -> Dict[str, int]: return { "modules": len(self.modules), "original_names": sum(len(m) for m in self.modules.values()), "unique_obfuscated": len(self.global_map), } - pyobfus/core/mapping.py:152-182 (helper)Helper: ObfuscationMapping.load() reads and parses the mapping JSON file from disk, supporting version validation and global_map backfill.
def load(cls, path: Union[str, Path]) -> "ObfuscationMapping": """Load mapping from a JSON file written by `save()`.""" path = Path(path) if not path.exists(): raise FileNotFoundError(f"Mapping file not found: {path}") data = json.loads(path.read_text(encoding="utf-8")) version = data.get("version") if version != MAPPING_FORMAT_VERSION: raise ValueError( f"Unsupported mapping version {version!r}. " f"This pyobfus expects version {MAPPING_FORMAT_VERSION}." ) m = cls( root=data.get("root", ""), mode=data.get("mode", "single_file"), pyobfus_version=data.get("pyobfus_version", ""), created_at=data.get("created_at", ""), modules={mod: dict(exports) for mod, exports in data.get("modules", {}).items()}, ) for obf, info in data.get("global", {}).items(): m.global_map[obf] = (info.get("module", ""), info.get("original", "")) # Backfill global_map if it was not present (forward-only format) if not m.global_map: for mod, exports in m.modules.items(): for original, obfuscated in exports.items(): m.global_map.setdefault(obfuscated, (mod, original)) return m