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.
- pyobfus_mcp-0.1.0/PKG-INFO +173 -0
- pyobfus_mcp-0.1.0/README.md +142 -0
- pyobfus_mcp-0.1.0/pyobfus_mcp/__init__.py +26 -0
- pyobfus_mcp-0.1.0/pyobfus_mcp/server.py +144 -0
- pyobfus_mcp-0.1.0/pyobfus_mcp/tools.py +238 -0
- pyobfus_mcp-0.1.0/pyobfus_mcp.egg-info/PKG-INFO +173 -0
- pyobfus_mcp-0.1.0/pyobfus_mcp.egg-info/SOURCES.txt +12 -0
- pyobfus_mcp-0.1.0/pyobfus_mcp.egg-info/dependency_links.txt +1 -0
- pyobfus_mcp-0.1.0/pyobfus_mcp.egg-info/entry_points.txt +2 -0
- pyobfus_mcp-0.1.0/pyobfus_mcp.egg-info/requires.txt +6 -0
- pyobfus_mcp-0.1.0/pyobfus_mcp.egg-info/top_level.txt +1 -0
- pyobfus_mcp-0.1.0/pyproject.toml +59 -0
- pyobfus_mcp-0.1.0/setup.cfg +4 -0
- pyobfus_mcp-0.1.0/tests/test_tools.py +225 -0
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -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,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)
|