@privacyscrubber/mcp-server 1.0.0

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.
package/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # @privacyscrubber/mcp-server
2
+
3
+ **Zero-Trust Data Sanitization (ZTDS) Model Context Protocol (MCP) Server.**
4
+ Locally scrubs PII, secrets, credentials, and custom patterns from files and text contexts before sending them to LLMs.
5
+
6
+ ---
7
+
8
+ ## 🔒 Security & Privacy Guarantees
9
+
10
+ * **100% Offline / Local Execution:** All PII detection, regex matching, and tokenization happen entirely inside your local machine's RAM. No prompt context or file data is ever sent to any remote servers.
11
+ * **Volatile In-Memory Mapping:** The token replacement map (`sessionMap`) is kept in volatile RAM only. It is never persisted to disk, cookies, or database storage.
12
+ * **Support for Reverse-Scrubbing (Reveal):** Replaces tokens in LLM responses back with original values securely inside your local context.
13
+
14
+ ---
15
+
16
+ ## 🚀 Installation & Usage
17
+
18
+ ### 1. Instant Run with NPX
19
+ You can run the MCP server directly using `npx`:
20
+ ```bash
21
+ npx @privacyscrubber/mcp-server
22
+ ```
23
+
24
+ ---
25
+
26
+ ## ⚙️ Client Integrations
27
+
28
+ ### Claude Desktop
29
+ Add the server configuration to your Claude Desktop config file:
30
+ * **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
31
+ * **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
32
+
33
+ ```json
34
+ {
35
+ "mcpServers": {
36
+ "privacyscrubber": {
37
+ "command": "npx",
38
+ "args": ["-y", "@privacyscrubber/mcp-server"],
39
+ "env": {
40
+ "PRIVACYSCRUBBER_KEY": "YOUR_OPTIONAL_PRO_OR_TEAMS_KEY"
41
+ }
42
+ }
43
+ }
44
+ }
45
+ ```
46
+
47
+ ### Cursor / Windsurf
48
+ 1. Open settings (Settings -> Features -> MCP).
49
+ 2. Add new MCP server:
50
+ * **Name:** `privacyscrubber`
51
+ * **Type:** `command`
52
+ * **Command:** `npx -y @privacyscrubber/mcp-server`
53
+ 3. If you have a PRO key, add `PRIVACYSCRUBBER_KEY` as an environment variable in your system shell or configure it locally.
54
+
55
+ ---
56
+
57
+ ## 🛠️ Provided Tools
58
+
59
+ 1. `sanitize_text` — Sanitizes a raw string using the selected detection profile.
60
+ 2. `reveal_text` — Detokenizes a response containing PII tokens back to the original text.
61
+ 3. `sanitize_file` — Reads a local file, sanitizes its contents, and outputs the safe version (supports up to 10MB).
62
+
63
+ ---
64
+
65
+ ## 📄 License & Commercial Key
66
+ Standard use includes the **Free Tier** (limits to the `General` PII profile). To unlock 22+ specialized industry profiles (DevOps, Medical, Legal, Finance) and custom regex rules, acquire a license at [privacyscrubber.com/pricing](https://privacyscrubber.com/pricing).
package/index.js ADDED
@@ -0,0 +1,322 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * PrivacyScrubber MCP Server
5
+ * Zero-Trust Data Sanitization (ZTDS) for AI IDEs (Cursor, Windsurf) and Claude Desktop.
6
+ *
7
+ * Runs 100% locally. In-memory volatile token mapping. Zero server logs.
8
+ */
9
+
10
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
11
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
12
+ import {
13
+ CallToolRequestSchema,
14
+ ListToolsRequestSchema,
15
+ } from "@modelcontextprotocol/sdk/types.js";
16
+ import { createRequire } from 'module';
17
+ import fs from 'fs';
18
+ import path from 'path';
19
+ import { fileURLToPath } from 'url';
20
+
21
+ import crypto from 'crypto';
22
+
23
+ const require = createRequire(import.meta.url);
24
+ const __filename = fileURLToPath(import.meta.url);
25
+ const __dirname = path.dirname(__filename);
26
+
27
+ // Import the production core engine with 100% parity
28
+ const scrubberCorePath = path.resolve(__dirname, './scrubber-core.cjs');
29
+ const PrivacyScrubberCore = require(scrubberCorePath);
30
+
31
+ // Initialize core engine
32
+ PrivacyScrubberCore.init();
33
+
34
+ // Volatile in-memory token map
35
+ const sessionMap = {};
36
+
37
+ // Hardcoded public key for offline cryptographic verification
38
+ const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
39
+ MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw3f37srO402PU4++Baf8
40
+ FG8LY4l/IA3NKLlBnYmNHRTjfI/O/w5PDZn1xPcUQevojA1J+A5moKcjXsJ5b21X
41
+ hJoYSkE4vLpcVYOt1FhRwEHs1APDSyss0HixboLz2eW2XQf2NbwajWtNlyxvgczO
42
+ KE6ClnLomtsaKywwqB4alzdYnnnFJttFPjwmgPSO7D9AgN9sYaVkXOaOFrIZ90Ng
43
+ TRhSHUeL7ReltWlCHwz9xf5m2FrKtxr2VBlEoyPjsFzalHMey1EX+yXe81zM7IIi
44
+ t1Z8agLzo7WIfNBAIWmRlerTplaFFZrQgdF5g/Y0n8IIMZOtadgoY8E855psDNZV
45
+ 7wIDAQAB
46
+ -----END PUBLIC KEY-----`;
47
+
48
+ function checkLicenseStatus() {
49
+ const key = (process.env.PRIVACYSCRUBBER_KEY || "").trim();
50
+ if (!key) return { isPro: false, type: null, error: "No license key provided." };
51
+
52
+ try {
53
+ const [payloadBase64, signatureBase64] = key.split('.');
54
+ if (!payloadBase64 || !signatureBase64) {
55
+ return { isPro: false, type: null, error: "Invalid license key structure." };
56
+ }
57
+
58
+ const verifier = crypto.createVerify('SHA256');
59
+ verifier.update(payloadBase64);
60
+ const isVerified = verifier.verify(PUBLIC_KEY, signatureBase64, 'base64');
61
+
62
+ if (!isVerified) {
63
+ return { isPro: false, type: null, error: "Signature verification failed." };
64
+ }
65
+
66
+ const payload = JSON.parse(Buffer.from(payloadBase64, 'base64').toString('utf8'));
67
+
68
+ // Expiration check
69
+ if (payload.expires && payload.expires < Math.floor(Date.now() / 1000)) {
70
+ return {
71
+ isPro: false,
72
+ type: payload.type,
73
+ error: `License expired on ${new Date(payload.expires * 1000).toLocaleDateString()}`
74
+ };
75
+ }
76
+
77
+ return { isPro: true, type: payload.type, error: null };
78
+ } catch (e) {
79
+ return { isPro: false, type: null, error: "Error parsing license: " + e.message };
80
+ }
81
+ }
82
+
83
+ // Create the MCP server
84
+ const server = new Server(
85
+ {
86
+ name: "privacyscrubber-mcp",
87
+ version: "1.0.0",
88
+ },
89
+ {
90
+ capabilities: {
91
+ tools: {},
92
+ },
93
+ }
94
+ );
95
+
96
+ // List available tools
97
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
98
+ return {
99
+ tools: [
100
+ {
101
+ name: "sanitize_text",
102
+ description: "Locally scrubs PII, secrets, and credentials (like API keys, passwords, emails, phones, names) from code, logs, or text. Replaces them with safe placeholders (e.g., [EMAIL_1], [API_KEY_1]). Keep your data secure before passing it to any LLM.",
103
+ inputSchema: {
104
+ type: "object",
105
+ properties: {
106
+ text: {
107
+ type: "string",
108
+ description: "The raw text, code, or logs to sanitize."
109
+ },
110
+ profile: {
111
+ type: "string",
112
+ description: "The detection profile to use. Available: 'General' (Free), or PRO profiles: 'Dev' (Engineering/Code), 'Medical', 'Pharma', 'Legal', 'Compliance', 'CCPA', 'Finance', 'Bizops', 'Sales', 'WealthMgmt', 'Insurance', 'Accounting', 'HR', 'Security', 'Marketing', 'Support', 'RealEstate', 'Agents', 'Academic', 'Creative', 'Tech', 'Personal'. Defaults to 'General'."
113
+ }
114
+ },
115
+ required: ["text"]
116
+ }
117
+ },
118
+ {
119
+ name: "reveal_text",
120
+ description: "Replaces masked tokens (e.g., [EMAIL_1], [API_KEY_1]) in the LLM's response back with the original private data from the local volatile RAM-only session map.",
121
+ inputSchema: {
122
+ type: "object",
123
+ properties: {
124
+ text: {
125
+ type: "string",
126
+ description: "The AI generated response containing placeholders to restore."
127
+ }
128
+ },
129
+ required: ["text"]
130
+ }
131
+ },
132
+ {
133
+ name: "sanitize_file",
134
+ description: "Reads a local file, sanitizes its contents using the selected profile, and outputs the safe version for AI analysis. Securely keeps original identifiers in memory.",
135
+ inputSchema: {
136
+ type: "object",
137
+ properties: {
138
+ filePath: {
139
+ type: "string",
140
+ description: "Absolute path to the file to sanitize."
141
+ },
142
+ profile: {
143
+ type: "string",
144
+ description: "The detection profile to use. Available: 'General' (Free), or PRO profiles: 'Dev' (Engineering/Code), 'Medical', 'Pharma', 'Legal', 'Compliance', 'CCPA', 'Finance', 'Bizops', 'Sales', 'WealthMgmt', 'Insurance', 'Accounting', 'HR', 'Security', 'Marketing', 'Support', 'RealEstate', 'Agents', 'Academic', 'Creative', 'Tech', 'Personal'. Defaults to 'General'."
145
+ }
146
+ },
147
+ required: ["filePath"]
148
+ }
149
+ }
150
+ ]
151
+ };
152
+ });
153
+
154
+ // Handle tool execution calls
155
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
156
+ const { name, arguments: args } = request.params;
157
+
158
+ try {
159
+ if (name === "sanitize_text") {
160
+ const { text, profile = "General" } = args;
161
+ const targetProfile = profile.trim();
162
+
163
+ // Check tier gating for advanced profiles
164
+ const isAdvanced = targetProfile.toLowerCase() !== "general";
165
+ const license = checkLicenseStatus();
166
+ if (isAdvanced && !license.isPro) {
167
+ const reason = license.error ? ` (${license.error})` : "";
168
+ return {
169
+ content: [
170
+ {
171
+ type: "text",
172
+ text: `⚠️ [PrivacyScrubber] Advanced profile '${targetProfile}' is locked in the FREE tier. Falling back to 'General' profile.${reason}\nTo unlock 22+ advanced industry profiles, custom regex, and unlimited logs protection, set PRIVACYSCRUBBER_KEY to your PRO license key.\nGet a key at: https://privacyscrubber.com/pricing\n\n--- Sanitized Output (General Profile) ---\n`
173
+ },
174
+ {
175
+ type: "text",
176
+ text: performSanitization(text, "General")
177
+ }
178
+ ]
179
+ };
180
+ }
181
+
182
+ const sanitized = performSanitization(text, targetProfile);
183
+ return {
184
+ content: [
185
+ {
186
+ type: "text",
187
+ text: sanitized
188
+ }
189
+ ]
190
+ };
191
+ }
192
+
193
+ if (name === "reveal_text") {
194
+ const { text } = args;
195
+ const restored = PrivacyScrubberCore.unscrubText(text, sessionMap);
196
+ return {
197
+ content: [
198
+ {
199
+ type: "text",
200
+ text: restored.restoredText
201
+ }
202
+ ]
203
+ };
204
+ }
205
+
206
+ if (name === "sanitize_file") {
207
+ const { filePath, profile = "General" } = args;
208
+
209
+ const resolvedPath = path.resolve(filePath);
210
+ if (!fs.existsSync(resolvedPath)) {
211
+ return {
212
+ isError: true,
213
+ content: [
214
+ {
215
+ type: "text",
216
+ text: `Error: File not found at '${filePath}'`
217
+ }
218
+ ]
219
+ };
220
+ }
221
+
222
+ const stats = fs.statSync(resolvedPath);
223
+ if (!stats.isFile()) {
224
+ return {
225
+ isError: true,
226
+ content: [
227
+ {
228
+ type: "text",
229
+ text: `Error: Path '${filePath}' is not a regular file.`
230
+ }
231
+ ]
232
+ };
233
+ }
234
+
235
+ // Limit file size to 10MB to prevent Out of Memory DoS
236
+ const MAX_SIZE = 10 * 1024 * 1024;
237
+ if (stats.size > MAX_SIZE) {
238
+ return {
239
+ isError: true,
240
+ content: [
241
+ {
242
+ type: "text",
243
+ text: `Error: File is too large (${(stats.size / 1024 / 1024).toFixed(2)}MB). Maximum allowed size is 10MB.`
244
+ }
245
+ ]
246
+ };
247
+ }
248
+
249
+ const content = fs.readFileSync(resolvedPath, "utf8");
250
+ const targetProfile = profile.trim();
251
+ const isAdvanced = targetProfile.toLowerCase() !== "general";
252
+ const license = checkLicenseStatus();
253
+
254
+ let prefix = "";
255
+ let finalProfile = targetProfile;
256
+
257
+ if (isAdvanced && !license.isPro) {
258
+ const reason = license.error ? ` (${license.error})` : "";
259
+ prefix = `⚠️ [PrivacyScrubber] Advanced profile '${targetProfile}' is locked in the FREE tier. Falling back to 'General' profile.${reason}\nGet a PRO key at: https://privacyscrubber.com/pricing\n\n`;
260
+ finalProfile = "General";
261
+ }
262
+
263
+ const sanitized = performSanitization(content, finalProfile);
264
+ return {
265
+ content: [
266
+ {
267
+ type: "text",
268
+ text: `${prefix}${sanitized}`
269
+ }
270
+ ]
271
+ };
272
+ }
273
+
274
+ return {
275
+ isError: true,
276
+ content: [
277
+ {
278
+ type: "text",
279
+ text: `Unknown tool: ${name}`
280
+ }
281
+ ]
282
+ };
283
+ } catch (error) {
284
+ return {
285
+ isError: true,
286
+ content: [
287
+ {
288
+ type: "text",
289
+ text: `Error executing tool: ${error.message}`
290
+ }
291
+ ]
292
+ };
293
+ }
294
+ });
295
+
296
+ // Run helper
297
+ function performSanitization(text, profile) {
298
+ const result = PrivacyScrubberCore.scrubText(text, [], {}, profile, sessionMap);
299
+
300
+ // Update our volatile map with new matches
301
+ if (result.tokenMap) {
302
+ Object.entries(result.tokenMap).forEach(([token, original]) => {
303
+ sessionMap[token] = original;
304
+ });
305
+ }
306
+
307
+ return result.scrubbedText;
308
+ }
309
+
310
+ // Start the server transport
311
+ const transport = new StdioServerTransport();
312
+ server.connect(transport).catch((error) => {
313
+ console.error("Failed to connect MCP server transport:", error);
314
+ process.exit(1);
315
+ });
316
+
317
+ // Mark script executable on launch
318
+ try {
319
+ fs.chmodSync(__filename, '755');
320
+ } catch (e) {
321
+ // Silent fail if filesystem is read-only
322
+ }
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@privacyscrubber/mcp-server",
3
+ "version": "1.0.0",
4
+ "description": "Zero-Trust Data Sanitization (ZTDS) MCP Server for AI IDEs and Claude Desktop. Locally scrubs PII, secrets, and credentials before sending context to LLMs.",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "mcp-server": "index.js",
9
+ "privacyscrubber-mcp": "index.js"
10
+ },
11
+ "scripts": {
12
+ "start": "node index.js"
13
+ },
14
+ "dependencies": {
15
+ "@modelcontextprotocol/sdk": "^0.6.0"
16
+ },
17
+ "keywords": [
18
+ "mcp",
19
+ "mcp-server",
20
+ "privacy",
21
+ "security",
22
+ "pii-redactor",
23
+ "secrets-scanner",
24
+ "zero-trust",
25
+ "ztds",
26
+ "claude-desktop",
27
+ "cursor"
28
+ ],
29
+ "author": "Ilya Sibiryakov (Brand Me Web)",
30
+ "license": "MIT"
31
+ }