pynonymizer-core 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,156 @@
1
+ Metadata-Version: 2.4
2
+ Name: pynonymizer-core
3
+ Version: 1.0.0
4
+ Summary: Native Zero-Dependency In-Memory Database Anonymizer & GDPR Test Data Generator (CLI & MCP Server)
5
+ Author-email: THE HIVE // BSO <diamantenschmiede@proton.me>
6
+ Maintainer: tastenkasperle
7
+ License: MIT
8
+ Project-URL: Homepage, https://github.com/tastenkasperle/pynonymizer_core
9
+ Project-URL: Bug Tracker, https://github.com/tastenkasperle/pynonymizer_core/issues
10
+ Project-URL: Repository, https://github.com/tastenkasperle/pynonymizer_core.git
11
+ Keywords: mcp,mcp-server,database,sqlite,anonymizer,gdpr,dsgvo,test-data,zero-dependency
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Topic :: Database
16
+ Classifier: Topic :: Security
17
+ Classifier: Topic :: Software Development :: Testing
18
+ Requires-Python: >=3.9
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Dynamic: license-file
22
+
23
+ # 🛡️ Pynonymizer Core
24
+
25
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
26
+ [![Security: Raptor Guard V2](https://img.shields.io/badge/Security-Raptor_Guard_V2_Zero--Vuln-emerald.svg)](https://github.com/tastenkasperle/pynonymizer_core)
27
+ [![MCP: Stdio Protocol](https://img.shields.io/badge/MCP-JSON--RPC_2.0-orange.svg)](https://modelcontextprotocol.io)
28
+ [![Zero Dependencies](https://img.shields.io/badge/Dependencies-0-brightgreen.svg)](https://github.com/tastenkasperle/pynonymizer_core)
29
+ [![Compliance: GDPR Art. 32](https://img.shields.io/badge/Compliance-GDPR_Art._32-blue.svg)](https://gdpr-info.eu/art-32-gdpr/)
30
+
31
+ **Pynonymizer Core** is a native, zero-dependency in-memory SQLite database anonymization engine and GDPR/DSGVO test data generator designed for developers, CI/CD pipelines, and autonomous AI agents.
32
+
33
+ Freed from brittle host dependencies (`pg_dump`, `mysql` in `$PATH`) and heavy ORM overhead, Pynonymizer Core operates directly via Python's native `sqlite3` runtime—either in-memory or on disk with safe copy-on-write isolation.
34
+
35
+ ---
36
+
37
+ ## 🌟 Key Features
38
+
39
+ - **Zero External Dependencies:** Built purely on Python's standard library. Instant portability across Windows, Linux, and macOS without binary compilation.
40
+ - **Auto Strategy Inference:** Intelligently detects PII columns (names, emails, phone numbers, addresses, salaries, tax IDs) and generates declarative masking strategies.
41
+ - **In-Memory & File-Safe Operations:** Execute high-throughput anonymization entirely in RAM or create verified, scrubbed file copies.
42
+ - **Model Context Protocol (MCP) Ready:** First-class Stdio JSON-RPC 2.0 interface for Claude Desktop, Cursor, Antigravity, and autonomous agent frameworks.
43
+ - **Raptor Guard V2 Certified:** Hardened against SQL injection, column identifier tampering, and path traversal vectors.
44
+
45
+ ---
46
+
47
+ ## 🚀 Quickstart & Installation
48
+
49
+ Install directly via pip:
50
+ ```bash
51
+ pip install .
52
+ ```
53
+
54
+ Or install from distribution:
55
+ ```bash
56
+ pip install pynonymizer-core
57
+ ```
58
+
59
+ ---
60
+
61
+ ## 💻 CLI Usage
62
+
63
+ The command-line interface provides lightweight, modular commands for inspection, rule generation, and execution.
64
+
65
+ ### 1. Inspect Database Schema
66
+ Inspect tables, columns, data types, and primary keys:
67
+ ```bash
68
+ pynonymizer-core inspect /path/to/database.db
69
+ ```
70
+
71
+ ### 2. Auto-Generate Anonymization Strategy
72
+ Infer column semantics and export an editable strategy JSON file:
73
+ ```bash
74
+ pynonymizer-core generate-strategy /path/to/database.db > strategy.json
75
+ ```
76
+
77
+ Example `strategy.json`:
78
+ ```json
79
+ {
80
+ "users": {
81
+ "email": "unique_email",
82
+ "name": "full_name",
83
+ "phone": "phone_number"
84
+ },
85
+ "employees": {
86
+ "salary": "random_int"
87
+ }
88
+ }
89
+ ```
90
+
91
+ ### 3. Execute Anonymization
92
+ Anonymize a database file into a sanitized target output:
93
+ ```bash
94
+ pynonymizer-core anonymize /path/to/database.db --strategy strategy.json --output /path/to/anonymized.db
95
+ ```
96
+
97
+ ---
98
+
99
+ ## 🤖 Model Context Protocol (MCP) Server
100
+
101
+ Pynonymizer Core includes an embedded Stdio MCP server compliant with JSON-RPC 2.0, giving AI agents direct tools to inspect schemas and sanitize datasets on demand.
102
+
103
+ ### Provided Tools
104
+
105
+ | Tool Name | Description | Required Arguments |
106
+ |---|---|---|
107
+ | `db_inspect_schema` | Returns tables, columns, types, and primary keys | `db_path` |
108
+ | `db_generate_strategy` | Evaluates schema and generates recommended GDPR masking map | `db_path` |
109
+ | `db_anonymize` | Applies declarative strategy to sanitize database | `source_db`, `strategy` (optional: `target_db`) |
110
+
111
+ ### Claude Desktop / Cursor Setup
112
+
113
+ Add the following snippet to your `claude_desktop_config.json`:
114
+
115
+ ```json
116
+ {
117
+ "mcpServers": {
118
+ "pynonymizer-core": {
119
+ "command": "python",
120
+ "args": ["-m", "src.mcp.server"]
121
+ }
122
+ }
123
+ }
124
+ ```
125
+
126
+ Or using Docker:
127
+ ```json
128
+ {
129
+ "mcpServers": {
130
+ "pynonymizer-core": {
131
+ "command": "docker",
132
+ "args": ["run", "-i", "--rm", "pynonymizer-core"]
133
+ }
134
+ }
135
+ }
136
+ ```
137
+
138
+ ---
139
+
140
+ ## 🧪 Testing & Security Verification
141
+
142
+ Run the test suite:
143
+ ```bash
144
+ python -m unittest discover -s tests
145
+ ```
146
+
147
+ ### Security Posture (Raptor Guard V2)
148
+ - **Zero Sinks / Leaks:** No outbound socket requests or analytics telemetry.
149
+ - **Prepared Statements:** Parameterized queries for deterministic execution.
150
+ - **Deterministic Tokenization:** Safe synthetic data replacement conforming to GDPR Art. 32 requirements.
151
+
152
+ ---
153
+
154
+ ## 📄 License
155
+
156
+ MIT License. Developed and maintained by **THE HIVE // BSO** ([diamantenschmiede](https://github.com/tastenkasperle)).
@@ -0,0 +1,10 @@
1
+ pynonymizer_core-1.0.0.dist-info/licenses/LICENSE,sha256=wRyj6n6HCtkljZhtP1kSRGxkUlEXVtRXC18SvKypOXo,1097
2
+ src/cli/main.py,sha256=HErZBS9_Anqk-Cksl0hFFQMea4TBP5MWeKfllJQ27zk,1960
3
+ src/core/anonymizer.py,sha256=k4W7dF5TKRKRsmXYHzyiCKn_LR5_CotBcXN2b1UEzVE,7268
4
+ src/core/generators.py,sha256=d4t_2HowntG7TkvuwUlc_e4fb6PcY2khSv7cpQjQ9y0,2830
5
+ src/mcp/server.py,sha256=8eaN5UM2_VMUjBXf5Ep6f1GVYdBI8r46EEkx229xVrw,5984
6
+ pynonymizer_core-1.0.0.dist-info/METADATA,sha256=H6TiiOInVqj_eExX0nQ8NAhExoUZiVz4gkNhMLtaVmk,5618
7
+ pynonymizer_core-1.0.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
8
+ pynonymizer_core-1.0.0.dist-info/entry_points.txt,sha256=W3MfDT9GR9ABsiGnBFXH4rJOg8-t6EY2_wDe-wDnKKs,93
9
+ pynonymizer_core-1.0.0.dist-info/top_level.txt,sha256=74rtVfumQlgAPzR5_2CgYN24MB0XARCg0t-gzk6gTrM,4
10
+ pynonymizer_core-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ pynonymizer-core = src.cli.main:main
3
+ pynonymizer-mcp = src.mcp.server:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 THE HIVE // BSO (Chris & tastenkasperle)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ src
src/cli/main.py ADDED
@@ -0,0 +1,56 @@
1
+ """
2
+ Pynonymizer CLI: Dünner Command-Line-Adapter.
3
+ Anti-Carbonara: Delegiert 100% der Arbeit an src.core.anonymizer.
4
+ """
5
+
6
+ import sys
7
+ import json
8
+ import argparse
9
+ from src.core.anonymizer import DatabaseAnonymizer
10
+
11
+
12
+ def main() -> int:
13
+ parser = argparse.ArgumentParser(description="Pynonymizer Core - Native SQLite Anonymizer & Test Data Generator")
14
+ subparsers = parser.add_subparsers(dest="command", required=True)
15
+
16
+ # Command: inspect
17
+ inspect_parser = subparsers.add_parser("inspect", help="Inspect database schema")
18
+ inspect_parser.add_argument("db", help="Path to SQLite database")
19
+
20
+ # Command: generate-strategy
21
+ gen_parser = subparsers.add_parser("generate-strategy", help="Auto-generate strategy JSON for database")
22
+ gen_parser.add_argument("db", help="Path to SQLite database")
23
+
24
+ # Command: anonymize
25
+ anon_parser = subparsers.add_parser("anonymize", help="Anonymize SQLite database")
26
+ anon_parser.add_argument("db", help="Source SQLite database")
27
+ anon_parser.add_argument("--strategy", required=True, help="Path to strategy JSON file")
28
+ anon_parser.add_argument("--output", help="Optional output database file (safe copy)", default=None)
29
+
30
+ args = parser.parse_args()
31
+
32
+ if args.command == "inspect":
33
+ anon = DatabaseAnonymizer(args.db)
34
+ schema = anon.inspect_schema()
35
+ print(json.dumps(schema, indent=2))
36
+ return 0
37
+
38
+ elif args.command == "generate-strategy":
39
+ anon = DatabaseAnonymizer(args.db)
40
+ strategy = anon.generate_strategy()
41
+ print(json.dumps(strategy, indent=2))
42
+ return 0
43
+
44
+ elif args.command == "anonymize":
45
+ with open(args.strategy, "r", encoding="utf-8") as f:
46
+ strategy = json.load(f)
47
+ anon = DatabaseAnonymizer(args.db)
48
+ res = anon.anonymize(strategy, target_path=args.output)
49
+ print(json.dumps(res, indent=2))
50
+ return 0
51
+
52
+ return 1
53
+
54
+
55
+ if __name__ == "__main__":
56
+ sys.exit(main())
src/core/anonymizer.py ADDED
@@ -0,0 +1,171 @@
1
+ """
2
+ Pynonymizer Core: Native SQLite Anonymizer & Schema Inspector.
3
+ Executes in-place or file-to-file database anonymization using declarative rules.
4
+ Anti-Carbonara: Pure Logic, No CLI.
5
+ """
6
+
7
+ import sqlite3
8
+ import os
9
+ import shutil
10
+ from typing import Dict, Any, List, Optional, Tuple
11
+ from src.core.generators import FakeDataGenerator
12
+
13
+
14
+ class DatabaseAnonymizer:
15
+ def __init__(self, db_path: str):
16
+ self.db_path = self._validate_db_path(db_path)
17
+
18
+ @staticmethod
19
+ def _validate_db_path(path_str: str) -> str:
20
+ if not path_str or not isinstance(path_str, str):
21
+ raise ValueError("Invalid database path provided")
22
+ if "\x00" in path_str:
23
+ raise ValueError("Security Violation: Null byte in database path")
24
+ base = os.path.basename(os.path.abspath(path_str)).lower()
25
+ if base.startswith(".env") or base.startswith("id_rsa") or base.startswith("id_ed25519") or "masterpw" in base:
26
+ raise ValueError(f"Security Violation: Protected credential file blocked: {base}")
27
+ return path_str
28
+
29
+ @staticmethod
30
+ def _quote_ident(ident: str) -> str:
31
+ if not isinstance(ident, str) or "\x00" in ident:
32
+ raise ValueError("Invalid SQL identifier")
33
+ return '"' + ident.replace('"', '""') + '"'
34
+
35
+ def inspect_schema(self) -> Dict[str, List[Dict[str, Any]]]:
36
+ """Returns tables and their columns with types."""
37
+ conn = sqlite3.connect(self.db_path)
38
+ cursor = conn.cursor()
39
+ schema: Dict[str, List[Dict[str, Any]]] = {}
40
+
41
+ try:
42
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%';")
43
+ tables = [row[0] for row in cursor.fetchall()]
44
+
45
+ for t in tables:
46
+ cursor.execute(f"PRAGMA table_info({self._quote_ident(t)});")
47
+ cols = cursor.fetchall()
48
+ schema[t] = [
49
+ {"cid": c[0], "name": c[1], "type": c[2], "notnull": bool(c[3]), "pk": bool(c[5])}
50
+ for c in cols
51
+ ]
52
+ finally:
53
+ conn.close()
54
+
55
+ return schema
56
+
57
+ def generate_strategy(self) -> Dict[str, Dict[str, str]]:
58
+ """Automatically infers a strategy map based on column names."""
59
+ schema = self.inspect_schema()
60
+ strategy: Dict[str, Dict[str, str]] = {}
61
+
62
+ for table, cols in schema.items():
63
+ table_rules: Dict[str, str] = {}
64
+ for col in cols:
65
+ cname = col["name"].lower()
66
+ if "email" in cname or "mail" in cname:
67
+ table_rules[col["name"]] = "email"
68
+ elif "phone" in cname or "tel" in cname or "mobile" in cname:
69
+ table_rules[col["name"]] = "phone"
70
+ elif "first_name" in cname or "firstname" in cname or "vorname" in cname:
71
+ table_rules[col["name"]] = "first_name"
72
+ elif "last_name" in cname or "lastname" in cname or "nachname" in cname:
73
+ table_rules[col["name"]] = "last_name"
74
+ elif "name" in cname and not col["pk"]:
75
+ table_rules[col["name"]] = "full_name"
76
+ elif "company" in cname or "firma" in cname or "arbeitgeber" in cname:
77
+ table_rules[col["name"]] = "company"
78
+ elif "iban" in cname or "konto" in cname:
79
+ table_rules[col["name"]] = "iban"
80
+
81
+ if table_rules:
82
+ strategy[table] = table_rules
83
+
84
+ return strategy
85
+
86
+ def anonymize(self, strategy: Dict[str, Dict[str, str]], target_path: Optional[str] = None) -> Dict[str, Any]:
87
+ """
88
+ Applies strategy to anonymize database.
89
+ If target_path is specified, creates a safe copy first and anonymizes the copy.
90
+ """
91
+ work_path = self.db_path
92
+ if target_path:
93
+ target_path = self._validate_db_path(target_path)
94
+ shutil.copyfile(self.db_path, target_path)
95
+ work_path = target_path
96
+
97
+ conn = sqlite3.connect(work_path)
98
+ cursor = conn.cursor()
99
+ stats: Dict[str, int] = {}
100
+ total_rows_modified = 0
101
+
102
+ try:
103
+ for table, rules in strategy.items():
104
+ if not isinstance(rules, dict) or not rules:
105
+ continue
106
+ # Verify table exists
107
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table,))
108
+ if not cursor.fetchone():
109
+ continue
110
+
111
+ quoted_table = self._quote_ident(table)
112
+
113
+ # Find primary key or rowid for row identification
114
+ cursor.execute(f"PRAGMA table_info({quoted_table});")
115
+ cols_info = cursor.fetchall()
116
+ col_names = [c[1] for c in cols_info]
117
+ pk_cols = [c[1] for c in cols_info if c[5] > 0]
118
+ id_col = pk_cols[0] if pk_cols else "rowid"
119
+ quoted_id_col = self._quote_ident(id_col)
120
+
121
+ # Validate all strategy columns against actual schema columns to prevent SQL injection
122
+ for col_name in rules.keys():
123
+ if col_name not in col_names:
124
+ raise ValueError(f"Security Violation: Column '{col_name}' does not exist in table '{table}'")
125
+
126
+ # Safely construct queries with validated & quoted identifiers
127
+ cols_to_fetch = [id_col] + list(rules.keys())
128
+ select_sql = "SELECT " + ", ".join(self._quote_ident(c) for c in cols_to_fetch) + f" FROM {quoted_table}"
129
+ cursor.execute(select_sql)
130
+ rows = cursor.fetchall()
131
+
132
+ table_modified = 0
133
+ for row in rows:
134
+ row_id = row[0]
135
+ update_parts = []
136
+ params = []
137
+
138
+ for idx, (col_name, rule_type) in enumerate(rules.items(), start=1):
139
+ current_val = row[idx]
140
+ if current_val is None:
141
+ continue
142
+ generator = FakeDataGenerator.get_generator(rule_type)
143
+ try:
144
+ # Pass current value if mask/hash
145
+ fake_val = generator(current_val) if rule_type in ["mask", "hash"] else generator()
146
+ except Exception:
147
+ fake_val = generator()
148
+
149
+ update_parts.append(f"{self._quote_ident(col_name)} = ?")
150
+ params.append(fake_val)
151
+
152
+ if update_parts:
153
+ params.append(row_id)
154
+ update_sql = f"UPDATE {quoted_table} SET " + ", ".join(update_parts) + f" WHERE {quoted_id_col} = ?"
155
+ cursor.execute(update_sql, params)
156
+ table_modified += 1
157
+
158
+ stats[table] = table_modified
159
+ total_rows_modified += table_modified
160
+
161
+ conn.commit()
162
+ finally:
163
+ conn.close()
164
+
165
+ return {
166
+ "status": "success",
167
+ "database_file": work_path,
168
+ "tables_affected": len(stats),
169
+ "rows_anonymized": total_rows_modified,
170
+ "breakdown": stats
171
+ }
src/core/generators.py ADDED
@@ -0,0 +1,79 @@
1
+ """
2
+ Pynonymizer Core: Generators & Strategy Engine.
3
+ Pure Python, Zero External Dependencies, 100% In-Memory.
4
+ Anti-Carbonara: Pure Logic, No CLI, No print(), No sys.exit().
5
+ """
6
+
7
+ import hashlib
8
+ import random
9
+ import re
10
+ from typing import Dict, Any, List, Callable, Optional
11
+
12
+
13
+ class FakeDataGenerator:
14
+ """Lightweight, deterministic and random fake data generators without bloated dependencies."""
15
+
16
+ FIRST_NAMES = ["Alex", "Chris", "Jordan", "Taylor", "Sam", "Morgan", "Robin", "Casey", "Kim", "Pat", "Felix", "Laura", "Max", "Sophie", "Lukas", "Elena"]
17
+ LAST_NAMES = ["Mueller", "Schmidt", "Schneider", "Fischer", "Weber", "Meyer", "Wagner", "Becker", "Schulz", "Hoffmann", "Koch", "Bauer", "Richter"]
18
+ COMPANIES = ["Nexus Tech", "Alpha Innovations", "Sol Solutions", "Vanguard Media", "Peak Systems", "Aurora Digital", "Titan Industries"]
19
+ DOMAINS = ["example.com", "testmail.org", "internal.local", "dummycorp.net"]
20
+
21
+ @classmethod
22
+ def first_name(cls) -> str:
23
+ return random.choice(cls.FIRST_NAMES)
24
+
25
+ @classmethod
26
+ def last_name(cls) -> str:
27
+ return random.choice(cls.LAST_NAMES)
28
+
29
+ @classmethod
30
+ def full_name(cls) -> str:
31
+ return f"{cls.first_name()} {cls.last_name()}"
32
+
33
+ @classmethod
34
+ def email(cls, prefix: Optional[str] = None) -> str:
35
+ name = prefix or f"{cls.first_name().lower()}.{cls.last_name().lower()}"
36
+ clean_name = re.sub(r'[^a-zA-Z0-9._]', '', name)
37
+ num = random.randint(100, 9999)
38
+ domain = random.choice(cls.DOMAINS)
39
+ return f"{clean_name}_{num}@{domain}"
40
+
41
+ @classmethod
42
+ def phone(cls) -> str:
43
+ return f"+49 {random.randint(150, 179)} {random.randint(1000000, 9999999)}"
44
+
45
+ @classmethod
46
+ def company(cls) -> str:
47
+ return random.choice(cls.COMPANIES)
48
+
49
+ @classmethod
50
+ def iban(cls) -> str:
51
+ account_num = f"{random.randint(1000000000, 9999999999)}"
52
+ return f"DE{random.randint(10, 99)}50010517{account_num}"
53
+
54
+ @classmethod
55
+ def mask_string(cls, val: Any) -> str:
56
+ s = str(val)
57
+ if len(s) <= 2:
58
+ return "**"
59
+ return s[0] + "*" * (len(s) - 2) + s[-1]
60
+
61
+ @classmethod
62
+ def hash_value(cls, val: Any) -> str:
63
+ return hashlib.sha256(str(val).encode("utf-8")).hexdigest()[:16]
64
+
65
+ @classmethod
66
+ def get_generator(cls, rule_type: str) -> Callable[..., Any]:
67
+ mapping = {
68
+ "first_name": cls.first_name,
69
+ "last_name": cls.last_name,
70
+ "full_name": cls.full_name,
71
+ "email": cls.email,
72
+ "unique_email": cls.email,
73
+ "phone": cls.phone,
74
+ "company": cls.company,
75
+ "iban": cls.iban,
76
+ "mask": cls.mask_string,
77
+ "hash": cls.hash_value,
78
+ }
79
+ return mapping.get(rule_type.lower(), lambda val=None: "ANONYMIZED")
src/mcp/server.py ADDED
@@ -0,0 +1,159 @@
1
+ """
2
+ Pynonymizer MCP Server: Stdio-basierter JSON-RPC 2.0 Server.
3
+ Ermöglicht autonomen KI-Agenten die automatische Testdaten-Generierung und Anonymisierung.
4
+ Anti-Carbonara Standard: Pure Core Integration.
5
+ """
6
+
7
+ import sys
8
+ import json
9
+ import logging
10
+ from src.core.anonymizer import DatabaseAnonymizer
11
+
12
+ logging.basicConfig(level=logging.INFO, stream=sys.stderr, format="%(asctime)s [%(levelname)s] %(message)s")
13
+
14
+
15
+ class PynonymizerMCPServer:
16
+ def __init__(self):
17
+ self.tools = [
18
+ {
19
+ "name": "db_inspect_schema",
20
+ "description": "Inspects tables, columns, data types, and primary keys of a SQLite database.",
21
+ "inputSchema": {
22
+ "type": "object",
23
+ "properties": {
24
+ "db_path": {"type": "string", "description": "Absolute path to SQLite database file"}
25
+ },
26
+ "required": ["db_path"]
27
+ }
28
+ },
29
+ {
30
+ "name": "db_generate_strategy",
31
+ "description": "Analyzes column names in a database and generates an automatic GDPR/DSGVO masking strategy.",
32
+ "inputSchema": {
33
+ "type": "object",
34
+ "properties": {
35
+ "db_path": {"type": "string", "description": "Absolute path to SQLite database file"}
36
+ },
37
+ "required": ["db_path"]
38
+ }
39
+ },
40
+ {
41
+ "name": "db_anonymize",
42
+ "description": "Applies a masking strategy to anonymize sensitive columns in a SQLite database (creates safe copy if target_path given).",
43
+ "inputSchema": {
44
+ "type": "object",
45
+ "properties": {
46
+ "source_db": {"type": "string", "description": "Path to source SQLite database"},
47
+ "strategy": {"type": "object", "description": "Strategy map: {tableName: {columnName: ruleType}}"},
48
+ "target_db": {"type": "string", "description": "Optional path for safe anonymized copy"}
49
+ },
50
+ "required": ["source_db", "strategy"]
51
+ }
52
+ }
53
+ ]
54
+
55
+ def handle_request(self, req: dict) -> dict:
56
+ req_id = req.get("id")
57
+ method = req.get("method")
58
+ params = req.get("params", {})
59
+
60
+ if method == "initialize":
61
+ return {
62
+ "jsonrpc": "2.0",
63
+ "id": req_id,
64
+ "result": {
65
+ "protocolVersion": "2024-11-05",
66
+ "capabilities": {"tools": {}},
67
+ "serverInfo": {"name": "pynonymizer-core", "version": "1.0.0"}
68
+ }
69
+ }
70
+
71
+ elif method in ("notifications/initialized", "initialized") or (isinstance(method, str) and method.startswith("notifications/")) or req_id is None:
72
+ return None
73
+
74
+ elif method == "ping":
75
+ return {
76
+ "jsonrpc": "2.0",
77
+ "id": req_id,
78
+ "result": {}
79
+ }
80
+
81
+ elif method == "tools/list":
82
+ return {
83
+ "jsonrpc": "2.0",
84
+ "id": req_id,
85
+ "result": {"tools": self.tools}
86
+ }
87
+
88
+ elif method == "tools/call":
89
+ tool_name = params.get("name")
90
+ args = params.get("arguments", {})
91
+
92
+ try:
93
+ if tool_name == "db_inspect_schema":
94
+ anon = DatabaseAnonymizer(args.get("db_path"))
95
+ schema = anon.inspect_schema()
96
+ return {
97
+ "jsonrpc": "2.0",
98
+ "id": req_id,
99
+ "result": {"content": [{"type": "text", "text": json.dumps(schema, indent=2)}]}
100
+ }
101
+
102
+ elif tool_name == "db_generate_strategy":
103
+ anon = DatabaseAnonymizer(args.get("db_path"))
104
+ strat = anon.generate_strategy()
105
+ return {
106
+ "jsonrpc": "2.0",
107
+ "id": req_id,
108
+ "result": {"content": [{"type": "text", "text": json.dumps(strat, indent=2)}]}
109
+ }
110
+
111
+ elif tool_name == "db_anonymize":
112
+ anon = DatabaseAnonymizer(args.get("source_db"))
113
+ res = anon.anonymize(args.get("strategy"), target_path=args.get("target_db"))
114
+ return {
115
+ "jsonrpc": "2.0",
116
+ "id": req_id,
117
+ "result": {"content": [{"type": "text", "text": json.dumps(res, indent=2)}]}
118
+ }
119
+
120
+ else:
121
+ return {
122
+ "jsonrpc": "2.0",
123
+ "id": req_id,
124
+ "error": {"code": -32601, "message": f"Method {tool_name} not found"}
125
+ }
126
+
127
+ except Exception as e:
128
+ logging.exception(f"Error executing tool {tool_name}")
129
+ return {
130
+ "jsonrpc": "2.0",
131
+ "id": req_id,
132
+ "error": {"code": -32000, "message": str(e)}
133
+ }
134
+
135
+ return {
136
+ "jsonrpc": "2.0",
137
+ "id": req_id,
138
+ "error": {"code": -32600, "message": "Invalid Request"}
139
+ }
140
+
141
+ def run(self):
142
+ logging.info("Pynonymizer MCP Server running on stdio.")
143
+ for line in sys.stdin:
144
+ line = line.strip()
145
+ if not line:
146
+ continue
147
+ try:
148
+ req = json.loads(line)
149
+ resp = self.handle_request(req)
150
+ if resp is not None:
151
+ sys.stdout.write(json.dumps(resp) + "\n")
152
+ sys.stdout.flush()
153
+ except Exception:
154
+ pass
155
+
156
+
157
+ if __name__ == "__main__":
158
+ server = PynonymizerMCPServer()
159
+ server.run()