pyobfus-mcp 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,173 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyobfus-mcp
3
+ Version: 0.1.0
4
+ Summary: Model Context Protocol server for pyobfus — the Python obfuscator. Lets Claude Desktop, Claude Code, Cursor, Windsurf, and Zed call pyobfus tools (preflight risk check, zero-config init, reverse stack-trace mapping) directly from an agent conversation.
5
+ Author-email: Rong Zhu <zhurong0525@gmail.com>
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/zhurong2020/pyobfus
8
+ Project-URL: Repository, https://github.com/zhurong2020/pyobfus
9
+ Project-URL: Main Package, https://pypi.org/project/pyobfus/
10
+ Project-URL: AI Integration Guide, https://github.com/zhurong2020/pyobfus/blob/main/docs/AI_INTEGRATION_STRATEGY.md
11
+ Project-URL: MCP Registry, https://github.com/modelcontextprotocol/servers
12
+ Keywords: mcp,mcp-server,model-context-protocol,pyobfus,python-obfuscator,code-obfuscator,claude-code,claude-desktop,cursor,windsurf,zed,llm-tools,ai-agent,anthropic
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: Apache Software License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Topic :: Security
24
+ Requires-Python: >=3.10
25
+ Description-Content-Type: text/markdown
26
+ Requires-Dist: pyobfus>=0.4.0
27
+ Requires-Dist: mcp>=1.0.0
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest>=7.0; extra == "dev"
30
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
31
+
32
+ # pyobfus-mcp — Model Context Protocol server for pyobfus
33
+
34
+ **pyobfus-mcp** exposes [pyobfus](https://github.com/zhurong2020/pyobfus) — the Python obfuscator — to any MCP-capable AI coding agent: **Claude Desktop, Claude Code, Cursor, Windsurf, Zed**, and anything else that speaks the [Model Context Protocol](https://modelcontextprotocol.io/).
35
+
36
+ Once configured, you can say:
37
+
38
+ > "Check if this FastAPI project is safe to obfuscate, then generate a pyobfus.yaml for it."
39
+
40
+ and the agent will autonomously call `check_obfuscation_risks` and `generate_pyobfus_config` — no copy/paste of CLI commands, no manual config editing.
41
+
42
+ ## Tools exposed
43
+
44
+ | Tool | What it does |
45
+ |---|---|
46
+ | `check_obfuscation_risks(path)` | Pre-flight scan for `eval`/`exec`, dynamic attribute access, framework reflection. Returns severity counts, detected frameworks, and a suggested preset. |
47
+ | `generate_pyobfus_config(path, preset_override?, write?)` | Auto-detect framework → generate `pyobfus.yaml`. Returns the YAML text without writing by default; `write=True` persists to disk. |
48
+ | `unmap_stack_trace(trace, mapping_path)` | Reverse obfuscated identifiers in a production stack trace using a `mapping.json`. |
49
+ | `list_presets()` | Enumerate every preset (community / framework-aware / Pro). |
50
+ | `explain_preset(name)` | Describe what a named preset changes: exclusions, docstring handling, parameter preservation. |
51
+
52
+ All tools return dicts with a `status` field and an `ai_hint` field suggesting the next action.
53
+
54
+ ## Install
55
+
56
+ ```bash
57
+ pip install pyobfus-mcp
58
+ ```
59
+
60
+ This pulls `pyobfus` and the MCP Python SDK automatically.
61
+
62
+ ## Configure
63
+
64
+ ### Claude Desktop
65
+
66
+ Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
67
+
68
+ ```json
69
+ {
70
+ "mcpServers": {
71
+ "pyobfus": {
72
+ "command": "pyobfus-mcp"
73
+ }
74
+ }
75
+ }
76
+ ```
77
+
78
+ Restart Claude Desktop. The pyobfus tools appear in the tool list.
79
+
80
+ ### Cursor
81
+
82
+ Edit `~/.cursor/mcp.json`:
83
+
84
+ ```json
85
+ {
86
+ "mcpServers": {
87
+ "pyobfus": {
88
+ "command": "pyobfus-mcp"
89
+ }
90
+ }
91
+ }
92
+ ```
93
+
94
+ ### Windsurf
95
+
96
+ Edit `~/.codeium/windsurf/mcp_config.json`:
97
+
98
+ ```json
99
+ {
100
+ "mcpServers": {
101
+ "pyobfus": {
102
+ "command": "pyobfus-mcp"
103
+ }
104
+ }
105
+ }
106
+ ```
107
+
108
+ ### Zed
109
+
110
+ In `~/.config/zed/settings.json`:
111
+
112
+ ```json
113
+ {
114
+ "context_servers": {
115
+ "pyobfus": {
116
+ "command": {
117
+ "path": "pyobfus-mcp",
118
+ "args": []
119
+ }
120
+ }
121
+ }
122
+ }
123
+ ```
124
+
125
+ ### Claude Code
126
+
127
+ ```bash
128
+ claude mcp add pyobfus pyobfus-mcp
129
+ ```
130
+
131
+ ## Example session
132
+
133
+ ```
134
+ User: Can you check whether this Python project is safe to obfuscate?
135
+ Path: /Users/me/code/my-api
136
+
137
+ Agent: [invokes check_obfuscation_risks("/Users/me/code/my-api")]
138
+ I found 2 high-severity and 3 medium-severity patterns. FastAPI is
139
+ detected, so I'd suggest the `fastapi` preset. Want me to generate
140
+ the config?
141
+
142
+ User: Yes please, write it.
143
+
144
+ Agent: [invokes generate_pyobfus_config("/Users/me/code/my-api",
145
+ preset_override="fastapi", write=True)]
146
+ Wrote pyobfus.yaml. Next: pyobfus /Users/me/code/my-api -o dist/
147
+ -c pyobfus.yaml
148
+ ```
149
+
150
+ ## Debugging obfuscated code with your AI assistant
151
+
152
+ The killer feature: keep AI-assisted debugging even after you obfuscate.
153
+
154
+ ```
155
+ User: Here's a crash from prod. Can you help?
156
+ [pastes traceback full of I0, I1, I2...]
157
+
158
+ Agent: [invokes unmap_stack_trace(trace, "path/to/mapping.json")]
159
+ Reversed. The crash is in Calculator.add() called from
160
+ main() — 'Calculator' object has no attribute 'add_x'. Looks like
161
+ a typo in the method call site…
162
+ ```
163
+
164
+ ## License
165
+
166
+ Apache-2.0. Same as the main pyobfus package. The pyobfus Pro features remain license-gated; this MCP server only wraps the community-tier tools.
167
+
168
+ ## Links
169
+
170
+ - **Main package**: https://pypi.org/project/pyobfus/
171
+ - **Source**: https://github.com/zhurong2020/pyobfus
172
+ - **AI integration strategy**: [docs/AI_INTEGRATION_STRATEGY.md](https://github.com/zhurong2020/pyobfus/blob/main/docs/AI_INTEGRATION_STRATEGY.md)
173
+ - **MCP specification**: https://modelcontextprotocol.io/
@@ -0,0 +1,142 @@
1
+ # pyobfus-mcp — Model Context Protocol server for pyobfus
2
+
3
+ **pyobfus-mcp** exposes [pyobfus](https://github.com/zhurong2020/pyobfus) — the Python obfuscator — to any MCP-capable AI coding agent: **Claude Desktop, Claude Code, Cursor, Windsurf, Zed**, and anything else that speaks the [Model Context Protocol](https://modelcontextprotocol.io/).
4
+
5
+ Once configured, you can say:
6
+
7
+ > "Check if this FastAPI project is safe to obfuscate, then generate a pyobfus.yaml for it."
8
+
9
+ and the agent will autonomously call `check_obfuscation_risks` and `generate_pyobfus_config` — no copy/paste of CLI commands, no manual config editing.
10
+
11
+ ## Tools exposed
12
+
13
+ | Tool | What it does |
14
+ |---|---|
15
+ | `check_obfuscation_risks(path)` | Pre-flight scan for `eval`/`exec`, dynamic attribute access, framework reflection. Returns severity counts, detected frameworks, and a suggested preset. |
16
+ | `generate_pyobfus_config(path, preset_override?, write?)` | Auto-detect framework → generate `pyobfus.yaml`. Returns the YAML text without writing by default; `write=True` persists to disk. |
17
+ | `unmap_stack_trace(trace, mapping_path)` | Reverse obfuscated identifiers in a production stack trace using a `mapping.json`. |
18
+ | `list_presets()` | Enumerate every preset (community / framework-aware / Pro). |
19
+ | `explain_preset(name)` | Describe what a named preset changes: exclusions, docstring handling, parameter preservation. |
20
+
21
+ All tools return dicts with a `status` field and an `ai_hint` field suggesting the next action.
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ pip install pyobfus-mcp
27
+ ```
28
+
29
+ This pulls `pyobfus` and the MCP Python SDK automatically.
30
+
31
+ ## Configure
32
+
33
+ ### Claude Desktop
34
+
35
+ Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
36
+
37
+ ```json
38
+ {
39
+ "mcpServers": {
40
+ "pyobfus": {
41
+ "command": "pyobfus-mcp"
42
+ }
43
+ }
44
+ }
45
+ ```
46
+
47
+ Restart Claude Desktop. The pyobfus tools appear in the tool list.
48
+
49
+ ### Cursor
50
+
51
+ Edit `~/.cursor/mcp.json`:
52
+
53
+ ```json
54
+ {
55
+ "mcpServers": {
56
+ "pyobfus": {
57
+ "command": "pyobfus-mcp"
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ ### Windsurf
64
+
65
+ Edit `~/.codeium/windsurf/mcp_config.json`:
66
+
67
+ ```json
68
+ {
69
+ "mcpServers": {
70
+ "pyobfus": {
71
+ "command": "pyobfus-mcp"
72
+ }
73
+ }
74
+ }
75
+ ```
76
+
77
+ ### Zed
78
+
79
+ In `~/.config/zed/settings.json`:
80
+
81
+ ```json
82
+ {
83
+ "context_servers": {
84
+ "pyobfus": {
85
+ "command": {
86
+ "path": "pyobfus-mcp",
87
+ "args": []
88
+ }
89
+ }
90
+ }
91
+ }
92
+ ```
93
+
94
+ ### Claude Code
95
+
96
+ ```bash
97
+ claude mcp add pyobfus pyobfus-mcp
98
+ ```
99
+
100
+ ## Example session
101
+
102
+ ```
103
+ User: Can you check whether this Python project is safe to obfuscate?
104
+ Path: /Users/me/code/my-api
105
+
106
+ Agent: [invokes check_obfuscation_risks("/Users/me/code/my-api")]
107
+ I found 2 high-severity and 3 medium-severity patterns. FastAPI is
108
+ detected, so I'd suggest the `fastapi` preset. Want me to generate
109
+ the config?
110
+
111
+ User: Yes please, write it.
112
+
113
+ Agent: [invokes generate_pyobfus_config("/Users/me/code/my-api",
114
+ preset_override="fastapi", write=True)]
115
+ Wrote pyobfus.yaml. Next: pyobfus /Users/me/code/my-api -o dist/
116
+ -c pyobfus.yaml
117
+ ```
118
+
119
+ ## Debugging obfuscated code with your AI assistant
120
+
121
+ The killer feature: keep AI-assisted debugging even after you obfuscate.
122
+
123
+ ```
124
+ User: Here's a crash from prod. Can you help?
125
+ [pastes traceback full of I0, I1, I2...]
126
+
127
+ Agent: [invokes unmap_stack_trace(trace, "path/to/mapping.json")]
128
+ Reversed. The crash is in Calculator.add() called from
129
+ main() — 'Calculator' object has no attribute 'add_x'. Looks like
130
+ a typo in the method call site…
131
+ ```
132
+
133
+ ## License
134
+
135
+ Apache-2.0. Same as the main pyobfus package. The pyobfus Pro features remain license-gated; this MCP server only wraps the community-tier tools.
136
+
137
+ ## Links
138
+
139
+ - **Main package**: https://pypi.org/project/pyobfus/
140
+ - **Source**: https://github.com/zhurong2020/pyobfus
141
+ - **AI integration strategy**: [docs/AI_INTEGRATION_STRATEGY.md](https://github.com/zhurong2020/pyobfus/blob/main/docs/AI_INTEGRATION_STRATEGY.md)
142
+ - **MCP specification**: https://modelcontextprotocol.io/
@@ -0,0 +1,26 @@
1
+ """pyobfus-mcp — Model Context Protocol server for pyobfus.
2
+
3
+ Exposes pyobfus's core AI-native tools (preflight check, project init,
4
+ unmap, preset listing) to MCP-capable clients: Claude Desktop, Claude
5
+ Code, Cursor, Windsurf, Zed, and anything else that speaks MCP.
6
+
7
+ See README.md in this directory for install + configuration snippets.
8
+ """
9
+
10
+ __version__ = "0.1.0"
11
+
12
+ from pyobfus_mcp.tools import (
13
+ check_obfuscation_risks,
14
+ generate_pyobfus_config,
15
+ unmap_stack_trace,
16
+ list_presets,
17
+ explain_preset,
18
+ )
19
+
20
+ __all__ = [
21
+ "check_obfuscation_risks",
22
+ "generate_pyobfus_config",
23
+ "unmap_stack_trace",
24
+ "list_presets",
25
+ "explain_preset",
26
+ ]
@@ -0,0 +1,144 @@
1
+ """
2
+ MCP server entry point for pyobfus.
3
+
4
+ Uses the official Model Context Protocol Python SDK
5
+ (https://github.com/modelcontextprotocol/python-sdk). Install with:
6
+
7
+ pip install pyobfus-mcp
8
+
9
+ Registers the pyobfus tools as MCP tool handlers and runs over stdio
10
+ (the transport used by Claude Desktop, Claude Code, Cursor, Windsurf,
11
+ and Zed). The tool implementations are pure Python in
12
+ `pyobfus_mcp.tools` and are independently testable — this module is
13
+ just a thin adapter.
14
+
15
+ Configure in Claude Desktop (`~/Library/Application Support/Claude/claude_desktop_config.json`
16
+ on macOS, `%APPDATA%\\Claude\\claude_desktop_config.json` on Windows):
17
+
18
+ {
19
+ "mcpServers": {
20
+ "pyobfus": {
21
+ "command": "pyobfus-mcp"
22
+ }
23
+ }
24
+ }
25
+
26
+ Equivalent snippets for Cursor (`~/.cursor/mcp.json`), Windsurf, and
27
+ Zed are in `pyobfus_mcp/README.md`.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import sys
33
+ from typing import Any, Dict, Optional
34
+
35
+ from pyobfus_mcp import __version__
36
+ from pyobfus_mcp.tools import (
37
+ check_obfuscation_risks,
38
+ explain_preset,
39
+ generate_pyobfus_config,
40
+ list_presets,
41
+ unmap_stack_trace,
42
+ )
43
+
44
+
45
+ def _build_server() -> Any:
46
+ """Construct and return the MCP Server instance.
47
+
48
+ Imported lazily so callers can `from pyobfus_mcp.server import main`
49
+ even if the `mcp` SDK isn't installed (e.g., during unit tests of
50
+ tools.py).
51
+ """
52
+ try:
53
+ from mcp.server.fastmcp import FastMCP # type: ignore[import-not-found]
54
+ except ImportError as e: # pragma: no cover — runtime-only
55
+ raise SystemExit(
56
+ "The 'mcp' package is required to run the pyobfus-mcp server.\n"
57
+ "Install it with: pip install mcp\n"
58
+ f"(Original error: {e})"
59
+ )
60
+
61
+ app = FastMCP(name="pyobfus", version=__version__)
62
+
63
+ # Each decorated function becomes a named MCP tool. Descriptions
64
+ # come from the docstring of the underlying tools.* function.
65
+ @app.tool(
66
+ name="check_obfuscation_risks",
67
+ description=(
68
+ "Scan a Python project for patterns that may break obfuscation "
69
+ "(eval/exec, dynamic attribute access, framework reflection). "
70
+ "Returns severity counts, detected frameworks (FastAPI/Django/"
71
+ "Flask/Pydantic/Click/SQLAlchemy), and a suggested preset."
72
+ ),
73
+ )
74
+ def _check(path: str) -> Dict[str, Any]:
75
+ return check_obfuscation_risks(path)
76
+
77
+ @app.tool(
78
+ name="generate_pyobfus_config",
79
+ description=(
80
+ "Generate a pyobfus.yaml for a Python project. Auto-detects "
81
+ "frameworks and applies the matching preset. By default returns "
82
+ "the YAML text without writing to disk; set write=true to persist."
83
+ ),
84
+ )
85
+ def _init(
86
+ path: str, preset_override: Optional[str] = None, write: bool = False
87
+ ) -> Dict[str, Any]:
88
+ return generate_pyobfus_config(path, preset_override=preset_override, write=write)
89
+
90
+ @app.tool(
91
+ name="unmap_stack_trace",
92
+ description=(
93
+ "Reverse obfuscated identifiers in a stack trace using a "
94
+ "pyobfus mapping.json. Accepts the trace as plain text and the "
95
+ "path to a mapping file produced by --save-mapping."
96
+ ),
97
+ )
98
+ def _unmap(trace: str, mapping_path: str) -> Dict[str, Any]:
99
+ return unmap_stack_trace(trace, mapping_path)
100
+
101
+ @app.tool(
102
+ name="list_presets",
103
+ description=(
104
+ "List every pyobfus preset available, grouped by tier "
105
+ "(community / framework-aware / Pro)."
106
+ ),
107
+ )
108
+ def _list() -> Dict[str, Any]:
109
+ return list_presets()
110
+
111
+ @app.tool(
112
+ name="explain_preset",
113
+ description=(
114
+ "Describe what a named preset changes: exclude names count, "
115
+ "exclude patterns, preserve_param_names, docstring handling."
116
+ ),
117
+ )
118
+ def _explain(name: str) -> Dict[str, Any]:
119
+ return explain_preset(name)
120
+
121
+ return app
122
+
123
+
124
+ def main() -> None:
125
+ """Entry point invoked by the `pyobfus-mcp` console script."""
126
+ app = _build_server()
127
+ # FastMCP.run() defaults to stdio transport, which is what Claude
128
+ # Desktop / Cursor / Windsurf expect for locally-spawned servers.
129
+ app.run()
130
+
131
+
132
+ if __name__ == "__main__": # pragma: no cover
133
+ main()
134
+
135
+
136
+ # Backwards-compat export: some test harnesses look for a `tool_functions`
137
+ # list. Provide one that enumerates the underlying callable implementations.
138
+ tool_functions = [
139
+ check_obfuscation_risks,
140
+ generate_pyobfus_config,
141
+ unmap_stack_trace,
142
+ list_presets,
143
+ explain_preset,
144
+ ]
@@ -0,0 +1,238 @@
1
+ """
2
+ Pure-Python tool implementations for the pyobfus MCP server.
3
+
4
+ These functions are deliberately MCP-SDK-agnostic: they accept primitive
5
+ types, return dicts, and are independently testable without installing
6
+ the `mcp` package. `server.py` wraps them as MCP tools; external callers
7
+ (tests, CI, custom agent frameworks) can call them directly.
8
+
9
+ All return dicts follow a stable shape with a `status` field
10
+ ("success" | "error") and an `ai_hint` field containing the single next
11
+ command or action the calling agent should take.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from pathlib import Path
17
+ from typing import Any, Dict, Optional
18
+
19
+
20
+ def check_obfuscation_risks(path: str) -> Dict[str, Any]:
21
+ """Scan a Python project for patterns that may break obfuscation.
22
+
23
+ Wraps `pyobfus --check`. Returns a structured report with severity
24
+ counts, detected frameworks, a suggested preset, and an `ai_hint`
25
+ telling the calling agent the exact command to run next.
26
+
27
+ Args:
28
+ path: Path to a Python file or directory to scan.
29
+
30
+ Returns:
31
+ Dict with keys: status, files_scanned, severity_counts,
32
+ frameworks, suggested_preset, suggested_excludes, risks,
33
+ ai_hint, exit_code.
34
+ """
35
+ try:
36
+ from pyobfus.core.preflight import PreflightChecker
37
+ except ImportError as e:
38
+ return _error("PyobfusNotInstalled", str(e), "pip install pyobfus")
39
+
40
+ target = Path(path)
41
+ if not target.exists():
42
+ return _error(
43
+ "PathNotFound",
44
+ f"Path does not exist: {path}",
45
+ f"Double-check the path and try again.",
46
+ )
47
+
48
+ report = PreflightChecker().check_path(target)
49
+ payload = report.to_dict()
50
+ payload["status"] = "success" if payload.get("exit_code", 0) == 0 else "warnings"
51
+ return payload
52
+
53
+
54
+ def generate_pyobfus_config(
55
+ path: str, preset_override: Optional[str] = None, write: bool = False
56
+ ) -> Dict[str, Any]:
57
+ """Generate a pyobfus.yaml configuration for a Python project.
58
+
59
+ Wraps `pyobfus --init`. By default this returns the proposed YAML
60
+ *text* in the response without touching the filesystem, so an AI
61
+ agent can preview the config before asking the user whether to
62
+ write it. Set `write=True` to persist to disk at <path>/pyobfus.yaml.
63
+
64
+ Args:
65
+ path: Root of the Python project to scan.
66
+ preset_override: Optional preset name to force (safe, balanced,
67
+ aggressive, fastapi, django, flask, pydantic, click,
68
+ sqlalchemy). Default: auto-detected.
69
+ write: If True, writes the generated file to disk.
70
+
71
+ Returns:
72
+ Dict with keys: status, config_path, preset, excludes,
73
+ frameworks_detected, files_scanned, high_risk_findings, yaml,
74
+ written, ai_hint.
75
+ """
76
+ try:
77
+ from pyobfus.core.init_config import build_init_result
78
+ except ImportError as e:
79
+ return _error("PyobfusNotInstalled", str(e), "pip install pyobfus")
80
+
81
+ target = Path(path)
82
+ if not target.exists():
83
+ return _error(
84
+ "PathNotFound",
85
+ f"Path does not exist: {path}",
86
+ "Pass a valid project directory.",
87
+ )
88
+
89
+ result = build_init_result(target, preset_override=preset_override)
90
+
91
+ if write:
92
+ result.config_path.write_text(result.yaml_text, encoding="utf-8")
93
+ result.written = True
94
+
95
+ payload = result.to_dict()
96
+ payload["status"] = "success"
97
+ payload["yaml"] = result.yaml_text
98
+ payload["ai_hint"] = (
99
+ f"Review the YAML above. When ready, run "
100
+ f"'pyobfus {path} -o dist/ -c {result.config_path}'."
101
+ if not write
102
+ else f"Wrote {result.config_path}. Next: pyobfus {path} -o dist/ -c {result.config_path}"
103
+ )
104
+ return payload
105
+
106
+
107
+ def unmap_stack_trace(trace: str, mapping_path: str) -> Dict[str, Any]:
108
+ """Reverse obfuscated identifiers in a stack trace using a mapping.json.
109
+
110
+ Wraps `pyobfus --unmap`. Accepts the trace as a literal string (most
111
+ useful for agent workflows where the trace is already in the chat
112
+ buffer); for large logs, callers can pre-read the file and pass
113
+ its contents.
114
+
115
+ Args:
116
+ trace: Obfuscated stack trace or error log as plain text.
117
+ mapping_path: Filesystem path to a mapping.json produced by
118
+ `pyobfus ... --save-mapping PATH`.
119
+
120
+ Returns:
121
+ Dict with keys: status, original_trace, unmapped_trace,
122
+ mapping_stats, ai_hint.
123
+ """
124
+ try:
125
+ from pyobfus.core.mapping import ObfuscationMapping
126
+ except ImportError as e:
127
+ return _error("PyobfusNotInstalled", str(e), "pip install pyobfus")
128
+
129
+ mp = Path(mapping_path)
130
+ if not mp.exists():
131
+ return _error(
132
+ "MappingNotFound",
133
+ f"Mapping file not found: {mapping_path}",
134
+ "Generate one with: pyobfus src/ -o dist/ --save-mapping mapping.json",
135
+ )
136
+
137
+ try:
138
+ mapping = ObfuscationMapping.load(mp)
139
+ except (ValueError, OSError) as e:
140
+ return _error("InvalidMapping", str(e), "Regenerate the mapping file.")
141
+
142
+ unmapped = mapping.unmap_text(trace)
143
+ return {
144
+ "status": "success",
145
+ "original_trace": trace,
146
+ "unmapped_trace": unmapped,
147
+ "mapping_stats": mapping.stats(),
148
+ "ai_hint": (
149
+ "Names are reversed, but line numbers still point to the obfuscated "
150
+ "file. Cross-reference with the original source if needed."
151
+ ),
152
+ }
153
+
154
+
155
+ def list_presets() -> Dict[str, Any]:
156
+ """List every pyobfus preset available, grouped by tier.
157
+
158
+ Returns:
159
+ Dict with keys: status, community, framework, pro, ai_hint.
160
+ """
161
+ try:
162
+ from pyobfus.config import ObfuscationConfig
163
+ except ImportError as e:
164
+ return _error("PyobfusNotInstalled", str(e), "pip install pyobfus")
165
+
166
+ framework = sorted(ObfuscationConfig.FRAMEWORK_PRESETS)
167
+ all_presets = ObfuscationConfig.list_presets()
168
+ pro = {"trial", "commercial", "library", "maximum"}
169
+ community = [p for p in all_presets if p not in framework and p not in pro]
170
+
171
+ return {
172
+ "status": "success",
173
+ "community": community,
174
+ "framework": framework,
175
+ "pro": sorted(pro),
176
+ "ai_hint": (
177
+ "Framework presets are free and the recommended starting point "
178
+ "when your project imports fastapi/django/flask/pydantic/click/"
179
+ "sqlalchemy. Fall back to 'balanced' otherwise."
180
+ ),
181
+ }
182
+
183
+
184
+ def explain_preset(name: str) -> Dict[str, Any]:
185
+ """Describe what a named preset changes compared to balanced.
186
+
187
+ Returns the concrete exclude_names count, preserve_param_names,
188
+ remove_docstrings flag, and any framework-specific exclude patterns
189
+ so an AI agent can explain the preset to the user before applying it.
190
+
191
+ Args:
192
+ name: Preset name (e.g. "fastapi", "pydantic", "safe").
193
+
194
+ Returns:
195
+ Dict with keys: status, preset, level, exclude_names_count,
196
+ exclude_patterns, preserve_param_names, remove_docstrings,
197
+ ai_hint.
198
+ """
199
+ try:
200
+ from pyobfus.config import ObfuscationConfig
201
+ except ImportError as e:
202
+ return _error("PyobfusNotInstalled", str(e), "pip install pyobfus")
203
+
204
+ try:
205
+ cfg = ObfuscationConfig.get_preset(name)
206
+ except ValueError as e:
207
+ return _error("UnknownPreset", str(e), "Call list_presets to see valid names.")
208
+
209
+ return {
210
+ "status": "success",
211
+ "preset": name.lower(),
212
+ "level": cfg.level,
213
+ "exclude_names_count": len(cfg.exclude_names),
214
+ "exclude_patterns": list(cfg.exclude_patterns),
215
+ "preserve_param_names": cfg.preserve_param_names,
216
+ "remove_docstrings": cfg.remove_docstrings,
217
+ "string_encoding": cfg.string_encoding,
218
+ "ai_hint": (
219
+ f"Apply with: pyobfus src/ -o dist/ --preset {name.lower()}"
220
+ if cfg.level == "community"
221
+ else f"'{name}' is a Pro preset. Start a free trial: pyobfus-trial start"
222
+ ),
223
+ }
224
+
225
+
226
+ # ---------------------------------------------------------------------------
227
+ # Internal helpers
228
+ # ---------------------------------------------------------------------------
229
+
230
+
231
+ def _error(error_type: str, message: str, ai_hint: str) -> Dict[str, Any]:
232
+ """Build a standard error payload matching the CLI `--json` error shape."""
233
+ return {
234
+ "status": "error",
235
+ "error_type": error_type,
236
+ "message": message,
237
+ "ai_hint": ai_hint,
238
+ }
@@ -0,0 +1,173 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyobfus-mcp
3
+ Version: 0.1.0
4
+ Summary: Model Context Protocol server for pyobfus — the Python obfuscator. Lets Claude Desktop, Claude Code, Cursor, Windsurf, and Zed call pyobfus tools (preflight risk check, zero-config init, reverse stack-trace mapping) directly from an agent conversation.
5
+ Author-email: Rong Zhu <zhurong0525@gmail.com>
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/zhurong2020/pyobfus
8
+ Project-URL: Repository, https://github.com/zhurong2020/pyobfus
9
+ Project-URL: Main Package, https://pypi.org/project/pyobfus/
10
+ Project-URL: AI Integration Guide, https://github.com/zhurong2020/pyobfus/blob/main/docs/AI_INTEGRATION_STRATEGY.md
11
+ Project-URL: MCP Registry, https://github.com/modelcontextprotocol/servers
12
+ Keywords: mcp,mcp-server,model-context-protocol,pyobfus,python-obfuscator,code-obfuscator,claude-code,claude-desktop,cursor,windsurf,zed,llm-tools,ai-agent,anthropic
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: Apache Software License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Topic :: Security
24
+ Requires-Python: >=3.10
25
+ Description-Content-Type: text/markdown
26
+ Requires-Dist: pyobfus>=0.4.0
27
+ Requires-Dist: mcp>=1.0.0
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest>=7.0; extra == "dev"
30
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
31
+
32
+ # pyobfus-mcp — Model Context Protocol server for pyobfus
33
+
34
+ **pyobfus-mcp** exposes [pyobfus](https://github.com/zhurong2020/pyobfus) — the Python obfuscator — to any MCP-capable AI coding agent: **Claude Desktop, Claude Code, Cursor, Windsurf, Zed**, and anything else that speaks the [Model Context Protocol](https://modelcontextprotocol.io/).
35
+
36
+ Once configured, you can say:
37
+
38
+ > "Check if this FastAPI project is safe to obfuscate, then generate a pyobfus.yaml for it."
39
+
40
+ and the agent will autonomously call `check_obfuscation_risks` and `generate_pyobfus_config` — no copy/paste of CLI commands, no manual config editing.
41
+
42
+ ## Tools exposed
43
+
44
+ | Tool | What it does |
45
+ |---|---|
46
+ | `check_obfuscation_risks(path)` | Pre-flight scan for `eval`/`exec`, dynamic attribute access, framework reflection. Returns severity counts, detected frameworks, and a suggested preset. |
47
+ | `generate_pyobfus_config(path, preset_override?, write?)` | Auto-detect framework → generate `pyobfus.yaml`. Returns the YAML text without writing by default; `write=True` persists to disk. |
48
+ | `unmap_stack_trace(trace, mapping_path)` | Reverse obfuscated identifiers in a production stack trace using a `mapping.json`. |
49
+ | `list_presets()` | Enumerate every preset (community / framework-aware / Pro). |
50
+ | `explain_preset(name)` | Describe what a named preset changes: exclusions, docstring handling, parameter preservation. |
51
+
52
+ All tools return dicts with a `status` field and an `ai_hint` field suggesting the next action.
53
+
54
+ ## Install
55
+
56
+ ```bash
57
+ pip install pyobfus-mcp
58
+ ```
59
+
60
+ This pulls `pyobfus` and the MCP Python SDK automatically.
61
+
62
+ ## Configure
63
+
64
+ ### Claude Desktop
65
+
66
+ Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
67
+
68
+ ```json
69
+ {
70
+ "mcpServers": {
71
+ "pyobfus": {
72
+ "command": "pyobfus-mcp"
73
+ }
74
+ }
75
+ }
76
+ ```
77
+
78
+ Restart Claude Desktop. The pyobfus tools appear in the tool list.
79
+
80
+ ### Cursor
81
+
82
+ Edit `~/.cursor/mcp.json`:
83
+
84
+ ```json
85
+ {
86
+ "mcpServers": {
87
+ "pyobfus": {
88
+ "command": "pyobfus-mcp"
89
+ }
90
+ }
91
+ }
92
+ ```
93
+
94
+ ### Windsurf
95
+
96
+ Edit `~/.codeium/windsurf/mcp_config.json`:
97
+
98
+ ```json
99
+ {
100
+ "mcpServers": {
101
+ "pyobfus": {
102
+ "command": "pyobfus-mcp"
103
+ }
104
+ }
105
+ }
106
+ ```
107
+
108
+ ### Zed
109
+
110
+ In `~/.config/zed/settings.json`:
111
+
112
+ ```json
113
+ {
114
+ "context_servers": {
115
+ "pyobfus": {
116
+ "command": {
117
+ "path": "pyobfus-mcp",
118
+ "args": []
119
+ }
120
+ }
121
+ }
122
+ }
123
+ ```
124
+
125
+ ### Claude Code
126
+
127
+ ```bash
128
+ claude mcp add pyobfus pyobfus-mcp
129
+ ```
130
+
131
+ ## Example session
132
+
133
+ ```
134
+ User: Can you check whether this Python project is safe to obfuscate?
135
+ Path: /Users/me/code/my-api
136
+
137
+ Agent: [invokes check_obfuscation_risks("/Users/me/code/my-api")]
138
+ I found 2 high-severity and 3 medium-severity patterns. FastAPI is
139
+ detected, so I'd suggest the `fastapi` preset. Want me to generate
140
+ the config?
141
+
142
+ User: Yes please, write it.
143
+
144
+ Agent: [invokes generate_pyobfus_config("/Users/me/code/my-api",
145
+ preset_override="fastapi", write=True)]
146
+ Wrote pyobfus.yaml. Next: pyobfus /Users/me/code/my-api -o dist/
147
+ -c pyobfus.yaml
148
+ ```
149
+
150
+ ## Debugging obfuscated code with your AI assistant
151
+
152
+ The killer feature: keep AI-assisted debugging even after you obfuscate.
153
+
154
+ ```
155
+ User: Here's a crash from prod. Can you help?
156
+ [pastes traceback full of I0, I1, I2...]
157
+
158
+ Agent: [invokes unmap_stack_trace(trace, "path/to/mapping.json")]
159
+ Reversed. The crash is in Calculator.add() called from
160
+ main() — 'Calculator' object has no attribute 'add_x'. Looks like
161
+ a typo in the method call site…
162
+ ```
163
+
164
+ ## License
165
+
166
+ Apache-2.0. Same as the main pyobfus package. The pyobfus Pro features remain license-gated; this MCP server only wraps the community-tier tools.
167
+
168
+ ## Links
169
+
170
+ - **Main package**: https://pypi.org/project/pyobfus/
171
+ - **Source**: https://github.com/zhurong2020/pyobfus
172
+ - **AI integration strategy**: [docs/AI_INTEGRATION_STRATEGY.md](https://github.com/zhurong2020/pyobfus/blob/main/docs/AI_INTEGRATION_STRATEGY.md)
173
+ - **MCP specification**: https://modelcontextprotocol.io/
@@ -0,0 +1,12 @@
1
+ README.md
2
+ pyproject.toml
3
+ pyobfus_mcp/__init__.py
4
+ pyobfus_mcp/server.py
5
+ pyobfus_mcp/tools.py
6
+ pyobfus_mcp.egg-info/PKG-INFO
7
+ pyobfus_mcp.egg-info/SOURCES.txt
8
+ pyobfus_mcp.egg-info/dependency_links.txt
9
+ pyobfus_mcp.egg-info/entry_points.txt
10
+ pyobfus_mcp.egg-info/requires.txt
11
+ pyobfus_mcp.egg-info/top_level.txt
12
+ tests/test_tools.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pyobfus-mcp = pyobfus_mcp.server:main
@@ -0,0 +1,6 @@
1
+ pyobfus>=0.4.0
2
+ mcp>=1.0.0
3
+
4
+ [dev]
5
+ pytest>=7.0
6
+ pytest-cov>=4.0
@@ -0,0 +1 @@
1
+ pyobfus_mcp
@@ -0,0 +1,59 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pyobfus-mcp"
7
+ version = "0.1.0"
8
+ description = "Model Context Protocol server for pyobfus — the Python obfuscator. Lets Claude Desktop, Claude Code, Cursor, Windsurf, and Zed call pyobfus tools (preflight risk check, zero-config init, reverse stack-trace mapping) directly from an agent conversation."
9
+ readme = {file = "README.md", content-type = "text/markdown"}
10
+ requires-python = ">=3.10"
11
+ license = {text = "Apache-2.0"}
12
+ authors = [
13
+ {name = "Rong Zhu", email = "zhurong0525@gmail.com"}
14
+ ]
15
+ keywords = [
16
+ "mcp", "mcp-server", "model-context-protocol",
17
+ "pyobfus", "python-obfuscator", "code-obfuscator",
18
+ "claude-code", "claude-desktop", "cursor", "windsurf", "zed",
19
+ "llm-tools", "ai-agent", "anthropic",
20
+ ]
21
+ classifiers = [
22
+ "Development Status :: 4 - Beta",
23
+ "Intended Audience :: Developers",
24
+ "License :: OSI Approved :: Apache Software License",
25
+ "Programming Language :: Python :: 3",
26
+ "Programming Language :: Python :: 3.10",
27
+ "Programming Language :: Python :: 3.11",
28
+ "Programming Language :: Python :: 3.12",
29
+ "Programming Language :: Python :: 3.13",
30
+ "Programming Language :: Python :: 3.14",
31
+ "Topic :: Software Development :: Libraries :: Python Modules",
32
+ "Topic :: Security",
33
+ ]
34
+
35
+ dependencies = [
36
+ "pyobfus>=0.4.0",
37
+ "mcp>=1.0.0",
38
+ ]
39
+
40
+ [project.optional-dependencies]
41
+ dev = [
42
+ "pytest>=7.0",
43
+ "pytest-cov>=4.0",
44
+ ]
45
+
46
+ [project.urls]
47
+ Homepage = "https://github.com/zhurong2020/pyobfus"
48
+ Repository = "https://github.com/zhurong2020/pyobfus"
49
+ "Main Package" = "https://pypi.org/project/pyobfus/"
50
+ "AI Integration Guide" = "https://github.com/zhurong2020/pyobfus/blob/main/docs/AI_INTEGRATION_STRATEGY.md"
51
+ "MCP Registry" = "https://github.com/modelcontextprotocol/servers"
52
+
53
+ [project.scripts]
54
+ pyobfus-mcp = "pyobfus_mcp.server:main"
55
+
56
+ [tool.setuptools.packages.find]
57
+ where = ["."]
58
+ include = ["pyobfus_mcp*"]
59
+ exclude = ["tests*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,225 @@
1
+ """Tests for pyobfus_mcp.tools — the pure-Python tool implementations.
2
+
3
+ These do NOT require the `mcp` SDK to be installed. They exercise the
4
+ same functions that `server.py` wraps as MCP tool handlers.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import textwrap
11
+ from pathlib import Path
12
+
13
+ import yaml
14
+
15
+ from pyobfus_mcp.tools import (
16
+ check_obfuscation_risks,
17
+ explain_preset,
18
+ generate_pyobfus_config,
19
+ list_presets,
20
+ unmap_stack_trace,
21
+ )
22
+
23
+
24
+ def _write(tmp_path: Path, name: str, src: str) -> Path:
25
+ p = tmp_path / name
26
+ p.parent.mkdir(parents=True, exist_ok=True)
27
+ p.write_text(textwrap.dedent(src).lstrip(), encoding="utf-8")
28
+ return p
29
+
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # check_obfuscation_risks
33
+ # ---------------------------------------------------------------------------
34
+
35
+
36
+ def test_check_obfuscation_risks_clean_project(tmp_path: Path) -> None:
37
+ _write(tmp_path, "a.py", "def greet(name): return f'hi {name}'\n")
38
+ result = check_obfuscation_risks(str(tmp_path))
39
+ assert result["status"] == "success"
40
+ assert result["files_scanned"] == 1
41
+ assert result["exit_code"] == 0
42
+
43
+
44
+ def test_check_obfuscation_risks_flags_eval(tmp_path: Path) -> None:
45
+ _write(tmp_path, "a.py", "eval('x+1')\n")
46
+ result = check_obfuscation_risks(str(tmp_path))
47
+ assert result["status"] == "warnings"
48
+ assert result["severity_counts"]["high"] >= 1
49
+ assert result["ai_hint"]
50
+
51
+
52
+ def test_check_obfuscation_risks_detects_fastapi(tmp_path: Path) -> None:
53
+ _write(
54
+ tmp_path,
55
+ "api.py",
56
+ """
57
+ from fastapi import FastAPI
58
+ app = FastAPI()
59
+ """,
60
+ )
61
+ result = check_obfuscation_risks(str(tmp_path))
62
+ assert result["suggested_preset"] == "fastapi"
63
+ assert any(fw["name"] == "FastAPI" for fw in result["frameworks"])
64
+
65
+
66
+ def test_check_obfuscation_risks_path_not_found() -> None:
67
+ result = check_obfuscation_risks("/nonexistent/path/xyz123")
68
+ assert result["status"] == "error"
69
+ assert result["error_type"] == "PathNotFound"
70
+
71
+
72
+ # ---------------------------------------------------------------------------
73
+ # generate_pyobfus_config
74
+ # ---------------------------------------------------------------------------
75
+
76
+
77
+ def test_generate_pyobfus_config_returns_yaml_without_writing(tmp_path: Path) -> None:
78
+ _write(
79
+ tmp_path,
80
+ "m.py",
81
+ """
82
+ from pydantic import BaseModel
83
+ class U(BaseModel): x: int
84
+ """,
85
+ )
86
+ result = generate_pyobfus_config(str(tmp_path))
87
+ assert result["status"] == "success"
88
+ assert result["preset"] == "pydantic"
89
+ assert result["written"] is False
90
+ assert "yaml" in result
91
+ # YAML content is parseable
92
+ parsed = yaml.safe_load(result["yaml"])
93
+ assert parsed["obfuscation"]["preset"] == "pydantic"
94
+ # No file written
95
+ assert not (tmp_path / "pyobfus.yaml").exists()
96
+
97
+
98
+ def test_generate_pyobfus_config_writes_when_requested(tmp_path: Path) -> None:
99
+ _write(tmp_path, "a.py", "x = 1\n")
100
+ result = generate_pyobfus_config(str(tmp_path), write=True)
101
+ assert result["status"] == "success"
102
+ assert result["written"] is True
103
+ assert Path(result["config_path"]).exists()
104
+
105
+
106
+ def test_generate_pyobfus_config_preset_override(tmp_path: Path) -> None:
107
+ _write(
108
+ tmp_path,
109
+ "api.py",
110
+ """
111
+ from fastapi import FastAPI
112
+ app = FastAPI()
113
+ """,
114
+ )
115
+ result = generate_pyobfus_config(str(tmp_path), preset_override="aggressive")
116
+ assert result["preset"] == "aggressive"
117
+
118
+
119
+ def test_generate_pyobfus_config_path_not_found() -> None:
120
+ result = generate_pyobfus_config("/nonexistent/xyz")
121
+ assert result["status"] == "error"
122
+
123
+
124
+ # ---------------------------------------------------------------------------
125
+ # unmap_stack_trace
126
+ # ---------------------------------------------------------------------------
127
+
128
+
129
+ def test_unmap_stack_trace_roundtrip(tmp_path: Path) -> None:
130
+ mapping_path = tmp_path / "m.json"
131
+ mapping_path.write_text(
132
+ json.dumps(
133
+ {
134
+ "version": 1,
135
+ "modules": {"calc": {"Calculator": "I0", "add": "I1"}},
136
+ "global": {
137
+ "I0": {"module": "calc", "original": "Calculator"},
138
+ "I1": {"module": "calc", "original": "add"},
139
+ },
140
+ }
141
+ ),
142
+ encoding="utf-8",
143
+ )
144
+ trace = "AttributeError: 'I0' object has no attribute 'I1'"
145
+ result = unmap_stack_trace(trace, str(mapping_path))
146
+ assert result["status"] == "success"
147
+ assert result["unmapped_trace"] == (
148
+ "AttributeError: 'Calculator' object has no attribute 'add'"
149
+ )
150
+ assert result["mapping_stats"]["unique_obfuscated"] == 2
151
+
152
+
153
+ def test_unmap_stack_trace_missing_mapping() -> None:
154
+ result = unmap_stack_trace("foo", "/does/not/exist.json")
155
+ assert result["status"] == "error"
156
+ assert result["error_type"] == "MappingNotFound"
157
+
158
+
159
+ def test_unmap_stack_trace_invalid_mapping(tmp_path: Path) -> None:
160
+ bad = tmp_path / "bad.json"
161
+ bad.write_text(json.dumps({"version": 999}), encoding="utf-8")
162
+ result = unmap_stack_trace("foo", str(bad))
163
+ assert result["status"] == "error"
164
+ assert result["error_type"] == "InvalidMapping"
165
+
166
+
167
+ # ---------------------------------------------------------------------------
168
+ # list_presets + explain_preset
169
+ # ---------------------------------------------------------------------------
170
+
171
+
172
+ def test_list_presets_groups_correctly() -> None:
173
+ result = list_presets()
174
+ assert result["status"] == "success"
175
+ # Community (non-framework, non-pro)
176
+ for p in ("safe", "balanced", "aggressive"):
177
+ assert p in result["community"]
178
+ # Framework
179
+ for p in ("fastapi", "django", "flask", "pydantic", "click", "sqlalchemy"):
180
+ assert p in result["framework"]
181
+ # Pro
182
+ for p in ("trial", "commercial", "library", "maximum"):
183
+ assert p in result["pro"]
184
+
185
+
186
+ def test_explain_preset_community() -> None:
187
+ result = explain_preset("fastapi")
188
+ assert result["status"] == "success"
189
+ assert result["preset"] == "fastapi"
190
+ assert result["level"] == "community"
191
+ assert result["preserve_param_names"] is True
192
+ assert result["exclude_names_count"] > 0
193
+ assert "--preset fastapi" in result["ai_hint"]
194
+
195
+
196
+ def test_explain_preset_pro_mentions_trial() -> None:
197
+ result = explain_preset("commercial")
198
+ assert result["status"] == "success"
199
+ assert result["level"] == "pro"
200
+ assert "trial" in result["ai_hint"].lower()
201
+
202
+
203
+ def test_explain_preset_unknown() -> None:
204
+ result = explain_preset("totally_made_up")
205
+ assert result["status"] == "error"
206
+ assert result["error_type"] == "UnknownPreset"
207
+
208
+
209
+ # ---------------------------------------------------------------------------
210
+ # Server module is importable without the mcp SDK
211
+ # ---------------------------------------------------------------------------
212
+
213
+
214
+ def test_server_module_importable_without_mcp_sdk() -> None:
215
+ """`pyobfus_mcp.server` must import cleanly even if `mcp` is missing.
216
+
217
+ The SDK import happens lazily inside `_build_server()` so tests of
218
+ tools.py don't require the SDK. This protects the test suite from
219
+ breaking when the package is not installed in a test env.
220
+ """
221
+ import pyobfus_mcp.server as srv
222
+
223
+ # tool_functions export is independent of the SDK
224
+ assert srv.tool_functions
225
+ assert all(callable(fn) for fn in srv.tool_functions)