@privacyscrubber/mcp-server 1.0.1 โ†’ 1.0.2

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,52 @@
1
+ # Contributing to PrivacyScrubber MCP Server
2
+
3
+ We welcome contributions to the PrivacyScrubber Model Context Protocol (MCP) server integration, client configurations, and testing suites.
4
+
5
+ ---
6
+
7
+ ## ๐Ÿ› ๏ธ Development Setup
8
+
9
+ To set up a local development environment:
10
+
11
+ 1. **Clone the Repository:**
12
+ ```bash
13
+ git clone https://github.com/moxno/privacyscrubber-mcp.git
14
+ cd privacyscrubber-mcp
15
+ ```
16
+
17
+ 2. **Install Dependencies:**
18
+ ```bash
19
+ npm install
20
+ ```
21
+
22
+ 3. **Run the Server Locally:**
23
+ You can spin up the server transport using:
24
+ ```bash
25
+ node index.js
26
+ ```
27
+ *Note: The server uses StdIO transport, so it will sit waiting for JSON-RPC input on standard streams.*
28
+
29
+ ---
30
+
31
+ ## ๐Ÿงช Testing Your Changes
32
+
33
+ Before submitting a Pull Request, ensure that all integration and cryptographic verification tests pass cleanly.
34
+
35
+ ### 1. Cryptographic Signature Assertions
36
+ Verify that local license key validations and RSA signatures parse correctly:
37
+ ```bash
38
+ node test-mcp.js
39
+ ```
40
+
41
+ ### 2. End-to-End JSON-RPC Subprocess Suite
42
+ Run the full simulation test which spawns the server, registers protocol versions, lists capabilities, processes files (TXT and DOCX), and asserts binary file rejections:
43
+ ```bash
44
+ node test-mcp-deep.js
45
+ ```
46
+
47
+ ---
48
+
49
+ ## ๐Ÿ”’ Regex Rules & PII Detection Core
50
+ The core detection engine and regex rules are bundled inside `scrubber-core.cjs` which maintains parity with the PrivacyScrubber Chrome Extension and web application.
51
+
52
+ If you notice a false positive or an uncaught entity type (e.g. name matching edge cases), please report it directly through the main project issues or email us at `support@privacyscrubber.com` so we can sync the ruleset across all platforms.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ilya Sibiryakov (BrandMeWeb)
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.
package/README.md CHANGED
@@ -1,24 +1,42 @@
1
1
  # @privacyscrubber/mcp-server
2
2
 
3
+ [![NPM Version](https://img.shields.io/npm/v/@privacyscrubber/mcp-server?color=blue)](https://www.npmjs.com/package/@privacyscrubber/mcp-server)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![Smithery Compatible](https://smithery.ai/badge/@privacyscrubber/mcp-server)](https://smithery.ai/server/@privacyscrubber/mcp-server)
6
+ [![Security: 100% Local](https://img.shields.io/badge/Security-100%25%20Local-emerald)](https://privacyscrubber.com)
7
+ [![Parity: 100% Core Match](https://img.shields.io/badge/Parity-100%25%20Core%20Match-blueviolet)](https://privacyscrubber.com)
8
+
3
9
  **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.
10
+ Locally scrubs PII, secrets, credentials, and custom regex rules from files and text contexts before they reach remote LLM providers.
5
11
 
6
12
  ---
7
13
 
8
- ## ๐Ÿ”’ Security & Privacy Guarantees
14
+ ## ๐Ÿ”’ Zero-Trust Data Flow
15
+
16
+ All sensitive parameters, identifiers, and variables are intercepted locally inside your machine's RAM. They are replaced by tokens (e.g. `[EMAIL_1]`) before being sent to the AI. Once the AI responds, the tokens are safely swapped back to original values in your local context.
9
17
 
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.
18
+ ```text
19
+ [Raw Input / Files] โ”€โ”€> [MCP sanitize_text] โ”€โ”€> [Masked Tokens] โ”€โ”€> [LLM API]
20
+ โ”‚ โ”‚
21
+ (In-Memory Map) (Result)
22
+ โ”‚ โ”‚
23
+ [Original Output] <โ”€โ”€โ”€ [MCP reveal_text] <โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
24
+ ```
13
25
 
14
26
  ---
15
27
 
16
- ## ๐Ÿš€ Installation & Usage
28
+ ## ๐Ÿš€ Installation
29
+
30
+ ### 1. Install via Smithery
31
+ To automatically configure and run with your preferred client, install using Smithery:
32
+ ```bash
33
+ npx -y @smithery/cli install @privacyscrubber/mcp-server --write-to-clients
34
+ ```
17
35
 
18
- ### 1. Instant Run with NPX
19
- You can run the MCP server directly using `npx`:
36
+ ### 2. Instant Run with NPX
37
+ Run the server directly without local installation:
20
38
  ```bash
21
- npx @privacyscrubber/mcp-server
39
+ npx -y @privacyscrubber/mcp-server
22
40
  ```
23
41
 
24
42
  ---
@@ -26,7 +44,7 @@ npx @privacyscrubber/mcp-server
26
44
  ## โš™๏ธ Client Integrations
27
45
 
28
46
  ### Claude Desktop
29
- Add the server configuration to your Claude Desktop config file:
47
+ Add this to your Claude Desktop config file:
30
48
  * **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
31
49
  * **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
32
50
 
@@ -37,7 +55,7 @@ Add the server configuration to your Claude Desktop config file:
37
55
  "command": "npx",
38
56
  "args": ["-y", "@privacyscrubber/mcp-server"],
39
57
  "env": {
40
- "PRIVACYSCRUBBER_KEY": "YOUR_OPTIONAL_PRO_OR_TEAMS_KEY"
58
+ "PRIVACYSCRUBBER_KEY": "YOUR_OPTIONAL_PRO_LICENSE_KEY"
41
59
  }
42
60
  }
43
61
  }
@@ -45,22 +63,93 @@ Add the server configuration to your Claude Desktop config file:
45
63
  ```
46
64
 
47
65
  ### Cursor / Windsurf
48
- 1. Open settings (Settings -> Features -> MCP).
66
+ 1. Navigate to Settings -> Features -> MCP.
49
67
  2. Add new MCP server:
50
68
  * **Name:** `privacyscrubber`
51
69
  * **Type:** `command`
52
70
  * **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.
71
+ 3. Optional: Set `PRIVACYSCRUBBER_KEY` as an environment variable in your system shell.
72
+
73
+ ---
74
+
75
+ ## ๐Ÿ› ๏ธ Provided Tools & JSON-RPC Specifications
76
+
77
+ ### 1. `sanitize_text`
78
+ Redacts PII, secrets, API keys, and credentials from a text block and populates the volatile local replacement mapping.
79
+
80
+ * **Arguments:**
81
+ * `text` (string, required): The raw content or logs to sanitize.
82
+ * `profile` (string, optional): Gated industry detection profile (e.g., 'General', 'Dev', 'Medical', 'Legal', 'Compliance'). Defaults to 'General'.
83
+ * **JSON-RPC Call Example:**
84
+ ```json
85
+ {
86
+ "method": "tools/call",
87
+ "params": {
88
+ "name": "sanitize_text",
89
+ "arguments": {
90
+ "text": "Contact me at dev-key-1234 or jane.doe@company.com",
91
+ "profile": "General"
92
+ }
93
+ }
94
+ }
95
+ ```
96
+ * **Response Example:**
97
+ ```json
98
+ {
99
+ "content": [
100
+ {
101
+ "type": "text",
102
+ "text": "Contact me at [SECRET_1] or [EMAIL_1]"
103
+ }
104
+ ]
105
+ }
106
+ ```
107
+
108
+ ### 2. `reveal_text`
109
+ Detokenizes the AI response back to the original values locally.
110
+
111
+ * **Arguments:**
112
+ * `text` (string, required): The response from the LLM containing tokenized placeholders.
113
+ * **JSON-RPC Call Example:**
114
+ ```json
115
+ {
116
+ "method": "tools/call",
117
+ "params": {
118
+ "name": "reveal_text",
119
+ "arguments": {
120
+ "text": "Please reach out to [EMAIL_1] regarding the update."
121
+ }
122
+ }
123
+ }
124
+ ```
125
+ * **Response Example:**
126
+ ```json
127
+ {
128
+ "content": [
129
+ {
130
+ "type": "text",
131
+ "text": "Please reach out to jane.doe@company.com regarding the update."
132
+ }
133
+ ]
134
+ }
135
+ ```
136
+
137
+ ### 3. `sanitize_file`
138
+ Reads a local file, extracts text, sanitizes it, and returns the redacted template for LLM analysis.
139
+ * **Supported Formats:** Plain text (source code, logs, CSV, JSON, markdown) and Microsoft Word (`.docx`) documents.
140
+ * **Arguments:**
141
+ * `filePath` (string, required): Absolute file path to read and sanitize.
142
+ * `profile` (string, optional): The industry detection profile.
54
143
 
55
144
  ---
56
145
 
57
- ## ๐Ÿ› ๏ธ Provided Tools
146
+ ## ๐ŸŒ Browser Extension & Web Client
58
147
 
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).
148
+ Looking for real-time protection directly inside your web browser?
149
+ * **Chrome Extension:** Get the [PrivacyScrubber Chrome Extension](https://chromewebstore.google.com/detail/privacyscrubber-%E2%80%94-pii-red/pimoejgefeilajmmbpghifdmhdlkgjol) to sanitize prompts directly inside ChatGPT, Claude, and Gemini in real-time.
150
+ * **Web Sandbox:** Use the zero-server browser sanitization tools at [PrivacyScrubber Homepage](https://privacyscrubber.com/).
62
151
 
63
152
  ---
64
153
 
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).
154
+ ## ๐Ÿ“„ License & Commercial Upgrade
155
+ Standard use is free under 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 commercial license at [privacyscrubber.com/pricing](https://privacyscrubber.com/pricing).
package/SECURITY.md ADDED
@@ -0,0 +1,30 @@
1
+ # Security Policy (SECURITY.md)
2
+
3
+ PrivacyScrubber is built with a zero-server, client-side first architecture. We take data privacy and vulnerability management extremely seriously.
4
+
5
+ ---
6
+
7
+ ## ๐Ÿ”’ Security Posture & Guarantees
8
+
9
+ The PrivacyScrubber MCP server operates under a strict **Zero-Trust Data Sanitization (ZTDS)** design:
10
+ 1. **100% Offline / Local Redaction:** All PII extraction, pattern matching, and tokenization logic run locally inside your system's Node.js runtime process memory (volatile RAM).
11
+ 2. **No Telemetry & No Remote Logs:** The server contains zero network request code for telemetry, prompts transmission, or log collection.
12
+ 3. **Strict Gating Audits:** Cryptographic key checks (for PRO/TEAMS licenses) are validated locally on-device using a signed RSA public key signature verification. No license verification calls are made to any remote databases.
13
+
14
+ ### Verification (Airplane Mode Test)
15
+ You can verify the zero-network posture at any time:
16
+ 1. Disconnect your machine's internet access (Wi-Fi/Ethernet).
17
+ 2. Launch the MCP server locally using Cursor, Windsurf, or Claude Desktop.
18
+ 3. Execute file sanitization and detokenization. All tools remain 100% operational offline.
19
+
20
+ ---
21
+
22
+ ## ๐Ÿ› Reporting a Vulnerability
23
+
24
+ If you discover a potential security vulnerability (e.g. regex engine backtracking denial of service, memory leaks, or local file access overflows), please do not report it in the public GitHub Issues tracker.
25
+
26
+ Instead, report it privately through our coordinated disclosure channel:
27
+ - **Email:** `security@privacyscrubber.com`
28
+ - **Response SLA:** Our team reviews all reports within **24 hours** and will provide a status update and remediation timeline.
29
+
30
+ Thank you for helping keep the developer ecosystem secure!
package/index.js CHANGED
@@ -113,6 +113,20 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
113
113
  }
114
114
  },
115
115
  required: ["text"]
116
+ },
117
+ outputSchema: {
118
+ type: "object",
119
+ properties: {
120
+ text: {
121
+ type: "string",
122
+ description: "The sanitized text output."
123
+ }
124
+ },
125
+ required: ["text"]
126
+ },
127
+ annotations: {
128
+ priority: 1.0,
129
+ audience: ["developer", "user"]
116
130
  }
117
131
  },
118
132
  {
@@ -127,6 +141,20 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
127
141
  }
128
142
  },
129
143
  required: ["text"]
144
+ },
145
+ outputSchema: {
146
+ type: "object",
147
+ properties: {
148
+ text: {
149
+ type: "string",
150
+ description: "The detokenized text with original values restored."
151
+ }
152
+ },
153
+ required: ["text"]
154
+ },
155
+ annotations: {
156
+ priority: 1.0,
157
+ audience: ["developer", "user"]
130
158
  }
131
159
  },
132
160
  {
@@ -135,7 +163,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
135
163
  inputSchema: {
136
164
  type: "object",
137
165
  properties: {
138
- filePath: {
166
+ file_path: {
139
167
  type: "string",
140
168
  description: "Absolute path to the file to sanitize."
141
169
  },
@@ -144,7 +172,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
144
172
  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
173
  }
146
174
  },
147
- required: ["filePath"]
175
+ required: ["file_path"]
176
+ },
177
+ outputSchema: {
178
+ type: "object",
179
+ properties: {
180
+ text: {
181
+ type: "string",
182
+ description: "The sanitized content of the file."
183
+ }
184
+ },
185
+ required: ["text"]
186
+ },
187
+ annotations: {
188
+ priority: 1.0,
189
+ audience: ["developer", "user"]
148
190
  }
149
191
  }
150
192
  ]
@@ -169,7 +211,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
169
211
  content: [
170
212
  {
171
213
  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`
214
+ 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\nBrowser Extension: https://chromewebstore.google.com/detail/privacyscrubber-%E2%80%94-pii-red/pimoejgefeilajmmbpghifdmhdlkgjol\n\n--- Sanitized Output (General Profile) ---\n`
173
215
  },
174
216
  {
175
217
  type: "text",
@@ -204,7 +246,20 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
204
246
  }
205
247
 
206
248
  if (name === "sanitize_file") {
207
- const { filePath, profile = "General" } = args;
249
+ const rawPath = args.file_path || args.filePath;
250
+ if (!rawPath) {
251
+ return {
252
+ isError: true,
253
+ content: [
254
+ {
255
+ type: "text",
256
+ text: "Error: Missing required parameter 'file_path'."
257
+ }
258
+ ]
259
+ };
260
+ }
261
+ const { profile = "General" } = args;
262
+ const filePath = rawPath;
208
263
 
209
264
  const resolvedPath = path.resolve(filePath);
210
265
  if (!fs.existsSync(resolvedPath)) {
@@ -246,7 +301,71 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
246
301
  };
247
302
  }
248
303
 
249
- const content = fs.readFileSync(resolvedPath, "utf8");
304
+ if (resolvedPath.toLowerCase().endsWith(".docx")) {
305
+ try {
306
+ const mammoth = require('mammoth');
307
+ const result = await mammoth.extractRawText({ path: resolvedPath });
308
+ const content = result.value;
309
+ const targetProfile = profile.trim();
310
+ const isAdvanced = targetProfile.toLowerCase() !== "general";
311
+ const license = checkLicenseStatus();
312
+
313
+ let prefix = "";
314
+ let finalProfile = targetProfile;
315
+
316
+ if (isAdvanced && !license.isPro) {
317
+ const reason = license.error ? ` (${license.error})` : "";
318
+ 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\nBrowser Extension: https://chromewebstore.google.com/detail/privacyscrubber-%E2%80%94-pii-red/pimoejgefeilajmmbpghifdmhdlkgjol\n\n`;
319
+ finalProfile = "General";
320
+ }
321
+
322
+ const sanitized = performSanitization(content, finalProfile);
323
+ return {
324
+ content: [
325
+ {
326
+ type: "text",
327
+ text: `${prefix}${sanitized}`
328
+ }
329
+ ]
330
+ };
331
+ } catch (docxError) {
332
+ return {
333
+ isError: true,
334
+ content: [
335
+ {
336
+ type: "text",
337
+ text: `Error: Failed to parse DOCX file: ${docxError.message}`
338
+ }
339
+ ]
340
+ };
341
+ }
342
+ }
343
+
344
+ const buffer = fs.readFileSync(resolvedPath);
345
+
346
+ // Check for null bytes in the first 8KB to detect binary files
347
+ let isBinary = false;
348
+ const checkLen = Math.min(buffer.length, 8000);
349
+ for (let i = 0; i < checkLen; i++) {
350
+ if (buffer[i] === 0) {
351
+ isBinary = true;
352
+ break;
353
+ }
354
+ }
355
+
356
+ if (isBinary) {
357
+ return {
358
+ isError: true,
359
+ content: [
360
+ {
361
+ type: "text",
362
+ text: `Error: Binary file format detected. The local MCP server only supports plain text files (e.g., source code, logs, CSV, markdown, JSON). To sanitize complex formats like PDF or DOCX, please use the web interface at: https://privacyscrubber.com/`
363
+ }
364
+ ]
365
+ };
366
+ }
367
+
368
+ const content = buffer.toString("utf8");
250
369
  const targetProfile = profile.trim();
251
370
  const isAdvanced = targetProfile.toLowerCase() !== "general";
252
371
  const license = checkLicenseStatus();
@@ -256,7 +375,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
256
375
 
257
376
  if (isAdvanced && !license.isPro) {
258
377
  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`;
378
+ 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\nBrowser Extension: https://chromewebstore.google.com/detail/privacyscrubber-%E2%80%94-pii-red/pimoejgefeilajmmbpghifdmhdlkgjol\n\n`;
260
379
  finalProfile = "General";
261
380
  }
262
381
 
package/llms.txt ADDED
@@ -0,0 +1,37 @@
1
+ # PrivacyScrubber MCP Server
2
+
3
+ > Zero-Trust Data Sanitization (ZTDS) Model Context Protocol (MCP) Server. Locally scrubs PII, secrets, credentials, and custom patterns from text and files before they reach remote LLM APIs.
4
+
5
+ ## Main URL
6
+ - Repository: https://github.com/moxno/privacyscrubber-mcp
7
+ - Web sandbox: https://privacyscrubber.com/
8
+ - Chrome extension: https://chromewebstore.google.com/detail/privacyscrubber-%E2%80%94-pii-red/pimoejgefeilajmmbpghifdmhdlkgjol
9
+
10
+ ## Provided Tools
11
+
12
+ The server runs locally in StdIO transport and exposes three tools:
13
+
14
+ ### sanitize_text
15
+ Scrubs PII (names, emails, phones, locations), API keys, and passwords from input text.
16
+ - Inputs:
17
+ - `text` (string, required): Raw text block or log segment to sanitize.
18
+ - `profile` (string, optional): Gated industry detection profile (e.g. 'General', 'Dev', 'Medical', 'Legal', 'Compliance'). Defaults to 'General'.
19
+ - Output: Masked template string containing placeholders (e.g. `[EMAIL_1]`).
20
+
21
+ ### reveal_text
22
+ Detokenizes AI responses by restoring original PII values locally in-memory.
23
+ - Inputs:
24
+ - `text` (string, required): Response from the LLM containing tokens like `[EMAIL_1]`.
25
+ - Output: Detokenized restored string.
26
+
27
+ ### sanitize_file
28
+ Reads a local file, checks format compatibility, extracts raw text, and redacts PII.
29
+ - Supported Formats: Source code files, CSV, JSON, markdown, text logs, and Microsoft Word (`.docx`) documents.
30
+ - Binary formats (like PDF, ZIP) are automatically rejected with a redirection notice to prevent corrupting inputs.
31
+ - Inputs:
32
+ - `filePath` (string, required): Absolute local file path.
33
+ - `profile` (string, optional): Industry detection profile.
34
+ - Output: Sanitized file text template.
35
+
36
+ ## Environment Variables
37
+ - `PRIVACYSCRUBBER_KEY` (string, optional): License key to unlock advanced industry detection profiles. If not set, the server runs on the Free tier and defaults all sanitizations to the 'General' profile.
package/manifest.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "manifest_version": "0.3",
3
+ "name": "privacyscrubber-mcp",
4
+ "version": "1.0.1",
5
+ "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.",
6
+ "author": {
7
+ "name": "PrivacyScrubber",
8
+ "email": "legal@privacyscrubber.com"
9
+ },
10
+ "server": {
11
+ "type": "node",
12
+ "entry_point": "index.js",
13
+ "mcp_config": {
14
+ "command": "node",
15
+ "args": ["${__dirname}/index.js"],
16
+ "env": {
17
+ "PRIVACYSCRUBBER_KEY": "${user_config.PRIVACYSCRUBBER_KEY}"
18
+ }
19
+ }
20
+ },
21
+ "user_config": {
22
+ "PRIVACYSCRUBBER_KEY": {
23
+ "type": "string",
24
+ "title": "PrivacyScrubber License Key",
25
+ "description": "Optional PRO or TEAMS license key. Leave blank to use the Free tier."
26
+ }
27
+ }
28
+ }
package/package.json CHANGED
@@ -1,9 +1,13 @@
1
1
  {
2
2
  "name": "@privacyscrubber/mcp-server",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
+ "mcpName": "io.github.moxno/privacyscrubber-mcp",
4
5
  "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
6
  "main": "index.js",
6
7
  "type": "module",
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
7
11
  "bin": {
8
12
  "mcp-server": "index.js",
9
13
  "privacyscrubber-mcp": "index.js"
@@ -12,7 +16,8 @@
12
16
  "start": "node index.js"
13
17
  },
14
18
  "dependencies": {
15
- "@modelcontextprotocol/sdk": "^0.6.0"
19
+ "@modelcontextprotocol/sdk": "^0.6.0",
20
+ "mammoth": "^1.8.0"
16
21
  },
17
22
  "keywords": [
18
23
  "mcp",
@@ -26,6 +31,14 @@
26
31
  "claude-desktop",
27
32
  "cursor"
28
33
  ],
29
- "author": "Ilya Sibiryakov (Brand Me Web)",
30
- "license": "MIT"
34
+ "author": "Ilya Sibiryakov (BrandMeWeb)",
35
+ "license": "MIT",
36
+ "homepage": "https://privacyscrubber.com/pii-mcp/",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/moxno/privacyscrubber-mcp.git"
40
+ },
41
+ "bugs": {
42
+ "url": "https://github.com/moxno/privacyscrubber-mcp/issues"
43
+ }
31
44
  }
package/server.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.moxno/privacyscrubber-mcp",
4
+ "description": "Zero-Trust PII & secrets sanitizer. Locally scrubs data in-memory before sending context to LLMs.",
5
+ "repository": {
6
+ "url": "https://github.com/moxno/privacyscrubber-mcp",
7
+ "source": "github"
8
+ },
9
+ "version": "1.0.2",
10
+ "packages": [
11
+ {
12
+ "registryType": "npm",
13
+ "identifier": "@privacyscrubber/mcp-server",
14
+ "version": "1.0.2",
15
+ "transport": {
16
+ "type": "stdio"
17
+ },
18
+ "environmentVariables": [
19
+ {
20
+ "description": "Optional PRO or TEAMS license key to unlock specialized detection profiles.",
21
+ "isRequired": false,
22
+ "format": "string",
23
+ "isSecret": true,
24
+ "name": "PRIVACYSCRUBBER_KEY"
25
+ }
26
+ ]
27
+ }
28
+ ]
29
+ }
package/smithery.yaml ADDED
@@ -0,0 +1,20 @@
1
+ # Smithery.ai configuration for PrivacyScrubber MCP Server
2
+ startCommand:
3
+ type: "stdio"
4
+ configSchema:
5
+ type: "object"
6
+ properties:
7
+ PRIVACYSCRUBBER_KEY:
8
+ type: "string"
9
+ title: "PrivacyScrubber License Key"
10
+ description: "Optional PRO or TEAMS license key. Leave blank to run on the Free tier."
11
+ commandFunction: |-
12
+ (config) => ({
13
+ command: 'node',
14
+ args: ['./index.js'],
15
+ env: {
16
+ PRIVACYSCRUBBER_KEY: config.PRIVACYSCRUBBER_KEY || ''
17
+ }
18
+ })
19
+ exampleConfig:
20
+ PRIVACYSCRUBBER_KEY: ""
@@ -0,0 +1,209 @@
1
+ import { spawn } from 'child_process';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+
9
+ const indexScript = path.resolve(__dirname, 'index.js');
10
+
11
+ // Helper to send JSON-RPC and wait for response
12
+ async function runMcpSession() {
13
+ console.log("Starting MCP Server process...");
14
+
15
+ // Set license key to valid PRO key for advanced test
16
+ const validKey = "eyJ0eXBlIjoicHJvIiwiZXhwaXJlcyI6MjA5OTA2MjE3NCwiaXNzdWVkQXQiOjE3ODM3MDIxNzQsIm5vbmNlIjoiMmI4MTllN2ZmM2ZmOTFmYiJ9.ZmhaKDGf9vG0nTH0nyOy3EInsuUmzjBOk9IOyiqzt6Y5s3UG+2yRExuKBoeXWymbpJt3NIJNM9sVxxl+lcop5wqbi2LNtPY4MuwBRA7pO4nn3Bes5R0lxLbEYVE8Iiw3zbfK4uVYQ53BJ7El6JCeFKPJ5WbKUsPLsjb1Lr2iQzW3ODOMaM5jKdgAGcNpuWQ373D7SW7I03Jec9kvP5hL7j3u4DV8ZzuQFzy2Nh+uMH54suydg2sNIpxrRQyxpGv7rZjTO1KT+xzzqnjqX4Pein0e6GrC5E4JUEggADtXPFMKW5SEiWzeeirB5RUPMO/F927S5fFuOYHAfuar2FqErA==";
17
+
18
+ const mcpProcess = spawn('node', [indexScript], {
19
+ env: {
20
+ ...process.env,
21
+ PRIVACYSCRUBBER_KEY: validKey
22
+ }
23
+ });
24
+
25
+ let buffer = '';
26
+ const pendingRequests = new Map();
27
+ let nextId = 1;
28
+
29
+ mcpProcess.stdout.on('data', (data) => {
30
+ buffer += data.toString();
31
+ // Parse JSON-RPC delimited by newlines
32
+ let newlineIndex;
33
+ while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
34
+ const line = buffer.substring(0, newlineIndex).trim();
35
+ buffer = buffer.substring(newlineIndex + 1);
36
+ if (line) {
37
+ try {
38
+ const response = JSON.parse(line);
39
+ if (response.id && pendingRequests.has(response.id)) {
40
+ const resolve = pendingRequests.get(response.id);
41
+ pendingRequests.delete(response.id);
42
+ resolve(response);
43
+ }
44
+ } catch (e) {
45
+ console.error("Failed to parse incoming line as JSON:", line, e);
46
+ }
47
+ }
48
+ }
49
+ });
50
+
51
+ mcpProcess.stderr.on('data', (data) => {
52
+ console.error(`[Server Stderr]: ${data.toString().trim()}`);
53
+ });
54
+
55
+ const sendRequest = (method, params) => {
56
+ return new Promise((resolve) => {
57
+ const id = nextId++;
58
+ pendingRequests.set(id, resolve);
59
+ const requestObj = {
60
+ jsonrpc: '2.0',
61
+ id,
62
+ method,
63
+ params
64
+ };
65
+ mcpProcess.stdin.write(JSON.stringify(requestObj) + '\n');
66
+ });
67
+ };
68
+
69
+ try {
70
+ // 1. Initialize
71
+ console.log("--> Sending 'initialize' request...");
72
+ const initResponse = await sendRequest('initialize', {
73
+ protocolVersion: '2024-11-05',
74
+ capabilities: {},
75
+ clientInfo: { name: 'test-client', version: '1.0.0' }
76
+ });
77
+ console.log("<-- Initialize response received:", JSON.stringify(initResponse).substring(0, 150) + "...");
78
+
79
+ // 2. List tools
80
+ console.log("--> Sending 'tools/list' request...");
81
+ const toolsResponse = await sendRequest('tools/list', {});
82
+ const tools = toolsResponse.result?.tools || [];
83
+ console.log(`<-- Tools list received. Total tools: ${tools.length}`);
84
+ tools.forEach(t => console.log(` - Tool: ${t.name} (${t.description.substring(0, 60)}...)`));
85
+
86
+ if (tools.length !== 3) {
87
+ throw new Error(`Expected 3 tools, got ${tools.length}`);
88
+ }
89
+
90
+ // 3. Test sanitize_text (General profile)
91
+ console.log("--> Calling 'sanitize_text' with General profile...");
92
+ const rawText = "Contact John Doe at john.doe@example.com or phone 555-0199.";
93
+ const sanitizeResponse = await sendRequest('tools/call', {
94
+ name: 'sanitize_text',
95
+ arguments: {
96
+ text: rawText,
97
+ profile: 'General'
98
+ }
99
+ });
100
+ const sanitizedText = sanitizeResponse.result?.content?.[0]?.text;
101
+ console.log("<-- Sanitized Output:", sanitizedText);
102
+
103
+ if (sanitizedText.includes("john.doe@example.com") || sanitizedText.includes("555-0199")) {
104
+ throw new Error("Sanitization failed to redact email or phone number.");
105
+ }
106
+ console.log("โœ… Sanitize text success.");
107
+
108
+ // 4. Test reveal_text (detokenization)
109
+ console.log("--> Calling 'reveal_text' to restore tokens...");
110
+ const revealResponse = await sendRequest('tools/call', {
111
+ name: 'reveal_text',
112
+ arguments: {
113
+ text: `Please email ${sanitizedText.match(/\[EMAIL_\d+\]/)?.[0] || '[EMAIL_1]'} back.`
114
+ }
115
+ });
116
+ const revealedText = revealResponse.result?.content?.[0]?.text;
117
+ console.log("<-- Revealed Output:", revealedText);
118
+ if (!revealedText.includes("john.doe@example.com")) {
119
+ throw new Error("Reveal failed to restore the original email.");
120
+ }
121
+ console.log("โœ… Reveal text success.");
122
+
123
+ // 5. Test sanitize_file (text file)
124
+ const tempFile = path.resolve(__dirname, 'temp-test-file.txt');
125
+ fs.writeFileSync(tempFile, "API Key: secret-key-value-12345\nUser: jane.smith@company.com", "utf8");
126
+ console.log(`--> Calling 'sanitize_file' on text file: ${tempFile}`);
127
+
128
+ const fileResponse = await sendRequest('tools/call', {
129
+ name: 'sanitize_file',
130
+ arguments: {
131
+ filePath: tempFile,
132
+ profile: 'Dev'
133
+ }
134
+ });
135
+ fs.unlinkSync(tempFile);
136
+
137
+ const fileSanitized = fileResponse.result?.content?.[0]?.text;
138
+ console.log("<-- Sanitized File Output:", fileSanitized);
139
+ if (fileSanitized.includes("secret-key-value-12345") || fileSanitized.includes("jane.smith@company.com")) {
140
+ throw new Error("File sanitization failed to redact credentials.");
141
+ }
142
+ console.log("โœ… Sanitize file success.");
143
+
144
+ // 6. Test sanitize_file (binary file rejection)
145
+ const tempBinFile = path.resolve(__dirname, 'temp-binary-file.bin');
146
+ const binBuffer = Buffer.alloc(100);
147
+ binBuffer.write("GIF89a", 0);
148
+ binBuffer[10] = 0; // Null byte
149
+ fs.writeFileSync(tempBinFile, binBuffer);
150
+ console.log(`--> Calling 'sanitize_file' on binary file: ${tempBinFile}`);
151
+
152
+ const binFileResponse = await sendRequest('tools/call', {
153
+ name: 'sanitize_file',
154
+ arguments: {
155
+ filePath: tempBinFile,
156
+ profile: 'General'
157
+ }
158
+ });
159
+ fs.unlinkSync(tempBinFile);
160
+
161
+ const isBinError = binFileResponse.result?.isError || binFileResponse.error;
162
+ const binErrorText = binFileResponse.result?.content?.[0]?.text || binFileResponse.error?.message;
163
+ console.log("<-- Binary File Response Error status:", isBinError, "| message:", binErrorText);
164
+
165
+ if (!isBinError || !binErrorText?.includes("Binary file format detected")) {
166
+ throw new Error("Binary file check failed to reject binary file with clean message.");
167
+ }
168
+ console.log("โœ… Sanitize file binary rejection success.");
169
+
170
+ // 7. Test sanitize_file (DOCX file parsing support)
171
+ const docxSource = path.resolve(__dirname, '../PrivacyScrubber/outreach-campaigns/guest-posts/Techbullion_GuestPost.docx');
172
+ const docxDest = path.resolve(__dirname, 'fixture-test.docx');
173
+
174
+ if (fs.existsSync(docxSource)) {
175
+ fs.copyFileSync(docxSource, docxDest);
176
+ console.log(`--> Calling 'sanitize_file' on DOCX document: ${docxDest}`);
177
+
178
+ const docxResponse = await sendRequest('tools/call', {
179
+ name: 'sanitize_file',
180
+ arguments: {
181
+ filePath: docxDest,
182
+ profile: 'General'
183
+ }
184
+ });
185
+ fs.unlinkSync(docxDest);
186
+
187
+ const docxText = docxResponse.result?.content?.[0]?.text;
188
+ console.log("<-- DOCX Sanitized text preview (first 150 chars):", docxText?.substring(0, 150) + "...");
189
+
190
+ if (!docxText || docxResponse.result?.isError || docxText.includes("Error") || docxText.length < 50) {
191
+ throw new Error("DOCX document parsing failed or returned invalid text content.");
192
+ }
193
+ console.log("โœ… Sanitize DOCX document success.");
194
+ } else {
195
+ console.log("โš ๏ธ Skipped DOCX test case: fixture file not found in main project path.");
196
+ }
197
+
198
+ console.log("\n๐ŸŽ‰ All deep integration tests passed successfully!");
199
+ mcpProcess.kill();
200
+ process.exit(0);
201
+
202
+ } catch (error) {
203
+ console.error("โŒ Test failed:", error.message);
204
+ mcpProcess.kill();
205
+ process.exit(1);
206
+ }
207
+ }
208
+
209
+ runMcpSession();
package/test-mcp.js CHANGED
@@ -10,7 +10,7 @@ const require = createRequire(import.meta.url);
10
10
  const __filename = fileURLToPath(import.meta.url);
11
11
  const __dirname = path.dirname(__filename);
12
12
 
13
- const scrubberCorePath = path.resolve(__dirname, '../chrome-extension/scrubber-core.js');
13
+ const scrubberCorePath = path.resolve(__dirname, './scrubber-core.cjs');
14
14
  const mcpServerIndex = path.resolve(__dirname, 'index.js');
15
15
 
16
16
  console.log("Loading modules for verification test...");