@privacyscrubber/mcp-server 1.0.5 → 1.6.7
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 +235 -25
- package/index.js +224 -54
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/scrubber-core.cjs +1 -1
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -1,28 +1,53 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @privacyscrubber/mcp-server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@privacyscrubber/mcp-server)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://smithery.ai/servers/privacyscrubber/pii-masking-mcp)
|
|
6
|
+
[](https://privacyscrubber.com)
|
|
7
|
+
[](https://privacyscrubber.com)
|
|
4
8
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
- **Microsoft Word (.docx) Support**: Enhanced the `sanitize_file` tool to parse and redact sensitive information from Word documents locally.
|
|
8
|
-
- **Unified Branding & Metadata**: Corrected repository fields and aligned server identity across NPM, Smithery, and Glama registries.
|
|
9
|
-
- **Enhanced Scanner Discovery**: Implemented static `server-card.json` configurations to support automated registry scanning.
|
|
9
|
+
**Zero-Trust Data Sanitization (ZTDS) Model Context Protocol (MCP) Server.**
|
|
10
|
+
Locally scrubs PII, secrets, credentials, and custom regex rules from files and text contexts before they reach remote LLM providers.
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
- **100% Local Processing**: All regex scanning, PII tokenization, and reverse-scrubbing occur directly in your machine's RAM.
|
|
13
|
-
- **Airplane Mode Verified**: Fully operational without an internet connection after the initial download.
|
|
14
|
-
- **Zero Server Logs**: No data, credentials, or prompts are sent to external APIs or remote databases.
|
|
12
|
+
---
|
|
15
13
|
|
|
16
|
-
##
|
|
17
|
-
|
|
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.
|
|
17
|
+
|
|
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
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
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
|
+
```
|
|
35
|
+
|
|
36
|
+
### 2. Instant Run with NPX
|
|
37
|
+
Run the server directly without local installation:
|
|
18
38
|
```bash
|
|
19
39
|
npx -y @privacyscrubber/mcp-server
|
|
20
40
|
```
|
|
21
41
|
|
|
22
|
-
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## ⚙️ Client Integrations
|
|
23
45
|
|
|
24
46
|
### Claude Desktop
|
|
25
|
-
Add to your
|
|
47
|
+
Add this to your Claude Desktop config file:
|
|
48
|
+
* **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
49
|
+
* **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
|
|
50
|
+
|
|
26
51
|
```json
|
|
27
52
|
{
|
|
28
53
|
"mcpServers": {
|
|
@@ -38,13 +63,198 @@ Add to your `claude_desktop_config.json`:
|
|
|
38
63
|
```
|
|
39
64
|
|
|
40
65
|
### Cursor / Windsurf
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
66
|
+
1. Navigate to Settings -> Features -> MCP.
|
|
67
|
+
2. Add new MCP server:
|
|
68
|
+
* **Name:** `privacyscrubber`
|
|
69
|
+
* **Type:** `command`
|
|
70
|
+
* **Command:** `npx -y @privacyscrubber/mcp-server`
|
|
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.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## 🌐 Browser Extension & Web Client
|
|
147
|
+
|
|
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/).
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## 📄 License & Commercial Upgrade
|
|
155
|
+
|
|
156
|
+
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).
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
### 4. `check_status`
|
|
161
|
+
|
|
162
|
+
Returns a visual dashboard showing your current tier, session request count, active profiles, and upgrade instructions. Use it at any time to check your license status or get setup help.
|
|
163
|
+
|
|
164
|
+
* **Arguments:** _(none required)_
|
|
165
|
+
* **JSON-RPC Call Example:**
|
|
166
|
+
```json
|
|
167
|
+
{
|
|
168
|
+
"method": "tools/call",
|
|
169
|
+
"params": { "name": "check_status", "arguments": {} }
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
* **Response Example (Free Tier):**
|
|
173
|
+
```
|
|
174
|
+
╔══════════════════════════════════════════════════╗
|
|
175
|
+
║ PrivacyScrubber MCP Server v1.6.6 ║
|
|
176
|
+
╠══════════════════════════════════════════════════╣
|
|
177
|
+
║ 🔓 Tier: FREE ║
|
|
178
|
+
║ 📊 Session requests: 5 ║
|
|
179
|
+
║ 📁 Input size limit: 50,000 characters per request║
|
|
180
|
+
╠══════════════════════════════════════════════════╣
|
|
181
|
+
║ 🏷️ Profiles: General only — PRO unlocks 22 more ║
|
|
182
|
+
║ 📋 Custom rules: 🔒 Locked — requires PRO ║
|
|
183
|
+
╠══════════════════════════════════════════════════╣
|
|
184
|
+
║ 💳 Upgrade to PRO — $110 Lifetime ║
|
|
185
|
+
║ https://privacyscrubber.com/pricing ║
|
|
186
|
+
╠══════════════════════════════════════════════════╣
|
|
187
|
+
║ After purchase, add your key to MCP config: ║
|
|
188
|
+
║ "PRIVACYSCRUBBER_KEY": "<your-key-here>" ║
|
|
189
|
+
║ Full setup guide: ║
|
|
190
|
+
║ https://privacyscrubber.com/features/mcp/ ║
|
|
191
|
+
╚══════════════════════════════════════════════════╝
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## 🔐 After Purchase: Activate PRO in Your MCP Client
|
|
197
|
+
|
|
198
|
+
After purchasing a PRO license at [privacyscrubber.com/pricing](https://privacyscrubber.com/pricing), you will receive a license key. Add it to your MCP client config as an environment variable: `PRIVACYSCRUBBER_KEY`.
|
|
199
|
+
|
|
200
|
+
### Claude Desktop
|
|
201
|
+
|
|
202
|
+
Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
|
|
203
|
+
|
|
204
|
+
```json
|
|
205
|
+
{
|
|
206
|
+
"mcpServers": {
|
|
207
|
+
"privacyscrubber": {
|
|
208
|
+
"command": "npx",
|
|
209
|
+
"args": ["-y", "@privacyscrubber/mcp-server"],
|
|
210
|
+
"env": {
|
|
211
|
+
"PRIVACYSCRUBBER_KEY": "YOUR_LICENSE_KEY_HERE"
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Restart Claude Desktop after saving.
|
|
219
|
+
|
|
220
|
+
### Cursor
|
|
221
|
+
|
|
222
|
+
1. Go to **Settings → Features → MCP Servers**.
|
|
223
|
+
2. Find `privacyscrubber` and click **Edit**.
|
|
224
|
+
3. Add the environment variable: `PRIVACYSCRUBBER_KEY=YOUR_LICENSE_KEY_HERE`.
|
|
225
|
+
4. Restart Cursor.
|
|
226
|
+
|
|
227
|
+
Alternatively, export it system-wide so all tools pick it up:
|
|
228
|
+
```bash
|
|
229
|
+
# macOS / Linux — add to ~/.zshrc or ~/.bashrc
|
|
230
|
+
export PRIVACYSCRUBBER_KEY="YOUR_LICENSE_KEY_HERE"
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Windsurf
|
|
234
|
+
|
|
235
|
+
Edit `~/.codeium/windsurf/mcp_config.json`:
|
|
236
|
+
|
|
237
|
+
```json
|
|
238
|
+
{
|
|
239
|
+
"mcpServers": {
|
|
240
|
+
"privacyscrubber": {
|
|
241
|
+
"command": "npx",
|
|
242
|
+
"args": ["-y", "@privacyscrubber/mcp-server"],
|
|
243
|
+
"env": {
|
|
244
|
+
"PRIVACYSCRUBBER_KEY": "YOUR_LICENSE_KEY_HERE"
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Verify Activation
|
|
252
|
+
|
|
253
|
+
After adding the key, ask your AI agent to call `check_status`:
|
|
254
|
+
|
|
255
|
+
```
|
|
256
|
+
Use the check_status tool from PrivacyScrubber MCP
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
The dashboard should show **Tier: PRO** and all profiles unlocked.
|
|
260
|
+
|
package/index.js
CHANGED
|
@@ -80,11 +80,44 @@ function checkLicenseStatus() {
|
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
// ── Session request counter (volatile in-memory) ─────────────────────────────
|
|
84
|
+
let sessionRequestCount = 0;
|
|
85
|
+
const UPSELL_EVERY_N = 10; // show upsell nudge every N free-tier requests
|
|
86
|
+
|
|
87
|
+
// Build a clean upsell content block visible to the agent / user
|
|
88
|
+
function buildUpsellBlock(reason) {
|
|
89
|
+
const count = sessionRequestCount;
|
|
90
|
+
let lines = [
|
|
91
|
+
'─────────────────────────────────────────────',
|
|
92
|
+
'⚡ PrivacyScrubber PRO — Upgrade your AI setup',
|
|
93
|
+
'─────────────────────────────────────────────',
|
|
94
|
+
];
|
|
95
|
+
if (reason) lines.push(`ℹ️ ${reason}`);
|
|
96
|
+
lines.push(
|
|
97
|
+
`📊 Free session requests: ${count}`,
|
|
98
|
+
'🔒 PRO unlocks: 22 industry profiles (Dev, Medical, Legal…)',
|
|
99
|
+
' + Custom regex rules (privacyscrubber.json)',
|
|
100
|
+
' + Unlimited input size',
|
|
101
|
+
'',
|
|
102
|
+
'💳 PRO Lifetime: $110 → https://privacyscrubber.com/pricing',
|
|
103
|
+
'',
|
|
104
|
+
'After purchase, add your key to your MCP config:',
|
|
105
|
+
' "PRIVACYSCRUBBER_KEY": "<your-key-here>"',
|
|
106
|
+
'─────────────────────────────────────────────'
|
|
107
|
+
);
|
|
108
|
+
return { type: 'text', text: lines.join('\n') };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Decide if we should attach a soft periodic nudge (every N requests, free tier)
|
|
112
|
+
function shouldNudge(isPro) {
|
|
113
|
+
return !isPro && sessionRequestCount > 0 && sessionRequestCount % UPSELL_EVERY_N === 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
83
116
|
// Create the MCP server
|
|
84
117
|
const server = new Server(
|
|
85
118
|
{
|
|
86
119
|
name: "privacyscrubber/pii-masking-mcp",
|
|
87
|
-
version: "1.
|
|
120
|
+
version: "1.6.7",
|
|
88
121
|
},
|
|
89
122
|
{
|
|
90
123
|
capabilities: {
|
|
@@ -146,6 +179,15 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
146
179
|
},
|
|
147
180
|
required: ["file_path"]
|
|
148
181
|
}
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
name: "check_status",
|
|
185
|
+
description: "Returns the current PrivacyScrubber MCP tier, session usage, available profiles, and PRO upgrade instructions. Call this to see your license status or get setup help.",
|
|
186
|
+
inputSchema: {
|
|
187
|
+
type: "object",
|
|
188
|
+
properties: {},
|
|
189
|
+
required: []
|
|
190
|
+
}
|
|
149
191
|
}
|
|
150
192
|
]
|
|
151
193
|
};
|
|
@@ -157,41 +199,70 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
157
199
|
|
|
158
200
|
try {
|
|
159
201
|
if (name === "sanitize_text") {
|
|
160
|
-
const { text, profile = "General" } = args;
|
|
161
|
-
|
|
202
|
+
const { text, profile = "General" } = args || {};
|
|
203
|
+
if (text === undefined || text === null) {
|
|
204
|
+
return {
|
|
205
|
+
isError: true,
|
|
206
|
+
content: [{ type: "text", text: "Error: Missing required parameter 'text'. Provide the string to sanitize." }]
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
if (typeof text !== "string") {
|
|
210
|
+
return {
|
|
211
|
+
isError: true,
|
|
212
|
+
content: [{ type: "text", text: `Error: Parameter 'text' must be a string, got '${typeof text}'. Stringify objects before passing.` }]
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
const targetProfile = (profile || "General").trim();
|
|
162
216
|
|
|
163
217
|
// Check tier gating for advanced profiles
|
|
164
218
|
const isAdvanced = targetProfile.toLowerCase() !== "general";
|
|
165
219
|
const license = checkLicenseStatus();
|
|
220
|
+
let finalProfile = targetProfile;
|
|
221
|
+
const extraBlocks = [];
|
|
222
|
+
|
|
223
|
+
sessionRequestCount++;
|
|
224
|
+
|
|
166
225
|
if (isAdvanced && !license.isPro) {
|
|
167
226
|
const reason = license.error ? ` (${license.error})` : "";
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
227
|
+
process.stderr.write(`⚠️ [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`);
|
|
228
|
+
finalProfile = "General";
|
|
229
|
+
extraBlocks.push(buildUpsellBlock(`Profile '${targetProfile}' requires PRO. Using 'General' as fallback.`));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const { processedText, wasTruncated } = truncateIfFree(text, license.isPro);
|
|
233
|
+
if (wasTruncated) {
|
|
234
|
+
extraBlocks.push(buildUpsellBlock(`Input was truncated to 50,000 characters (Free Tier limit).`));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const sanitized = performSanitization(processedText, finalProfile);
|
|
238
|
+
|
|
239
|
+
// Periodic soft nudge every N free-tier requests (no trigger event needed)
|
|
240
|
+
if (!license.isPro && extraBlocks.length === 0 && sessionRequestCount % UPSELL_EVERY_N === 0) {
|
|
241
|
+
extraBlocks.push(buildUpsellBlock(null));
|
|
180
242
|
}
|
|
181
243
|
|
|
182
|
-
const sanitized = performSanitization(text, targetProfile);
|
|
183
244
|
return {
|
|
184
245
|
content: [
|
|
185
|
-
{
|
|
186
|
-
|
|
187
|
-
text: sanitized
|
|
188
|
-
}
|
|
246
|
+
{ type: "text", text: sanitized },
|
|
247
|
+
...extraBlocks
|
|
189
248
|
]
|
|
190
249
|
};
|
|
191
250
|
}
|
|
192
251
|
|
|
193
252
|
if (name === "reveal_text") {
|
|
194
|
-
const { text } = args;
|
|
253
|
+
const { text } = args || {};
|
|
254
|
+
if (text === undefined || text === null) {
|
|
255
|
+
return {
|
|
256
|
+
isError: true,
|
|
257
|
+
content: [{ type: "text", text: "Error: Missing required parameter 'text'. Provide the AI response containing placeholders to restore." }]
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
if (typeof text !== "string") {
|
|
261
|
+
return {
|
|
262
|
+
isError: true,
|
|
263
|
+
content: [{ type: "text", text: `Error: Parameter 'text' must be a string, got '${typeof text}'.` }]
|
|
264
|
+
};
|
|
265
|
+
}
|
|
195
266
|
const restored = PrivacyScrubberCore.unscrubText(text, sessionMap);
|
|
196
267
|
return {
|
|
197
268
|
content: [
|
|
@@ -268,33 +339,37 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
268
339
|
const isAdvanced = targetProfile.toLowerCase() !== "general";
|
|
269
340
|
const license = checkLicenseStatus();
|
|
270
341
|
|
|
271
|
-
let prefix = "";
|
|
272
342
|
let finalProfile = targetProfile;
|
|
343
|
+
const extraBlocks = [];
|
|
344
|
+
|
|
345
|
+
sessionRequestCount++;
|
|
273
346
|
|
|
274
347
|
if (isAdvanced && !license.isPro) {
|
|
275
348
|
const reason = license.error ? ` (${license.error})` : "";
|
|
276
|
-
|
|
349
|
+
process.stderr.write(`⚠️ [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`);
|
|
277
350
|
finalProfile = "General";
|
|
351
|
+
extraBlocks.push(buildUpsellBlock(`Profile '${targetProfile}' requires PRO. Using 'General' as fallback.`));
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const { processedText: processedContent, wasTruncated } = truncateIfFree(content, license.isPro);
|
|
355
|
+
if (wasTruncated) extraBlocks.push(buildUpsellBlock(`File content was truncated to 50,000 characters (Free Tier limit).`));
|
|
356
|
+
|
|
357
|
+
const sanitized = performSanitization(processedContent, finalProfile);
|
|
358
|
+
|
|
359
|
+
if (!license.isPro && extraBlocks.length === 0 && sessionRequestCount % UPSELL_EVERY_N === 0) {
|
|
360
|
+
extraBlocks.push(buildUpsellBlock(null));
|
|
278
361
|
}
|
|
279
362
|
|
|
280
|
-
const sanitized = performSanitization(content, finalProfile);
|
|
281
363
|
return {
|
|
282
364
|
content: [
|
|
283
|
-
{
|
|
284
|
-
|
|
285
|
-
text: `${prefix}${sanitized}`
|
|
286
|
-
}
|
|
365
|
+
{ type: "text", text: sanitized },
|
|
366
|
+
...extraBlocks
|
|
287
367
|
]
|
|
288
368
|
};
|
|
289
369
|
} catch (docxError) {
|
|
290
370
|
return {
|
|
291
371
|
isError: true,
|
|
292
|
-
content: [
|
|
293
|
-
{
|
|
294
|
-
type: "text",
|
|
295
|
-
text: `Error: Failed to parse DOCX file: ${docxError.message}`
|
|
296
|
-
}
|
|
297
|
-
]
|
|
372
|
+
content: [{ type: "text", text: `Error: Failed to parse DOCX file: ${docxError.message}` }]
|
|
298
373
|
};
|
|
299
374
|
}
|
|
300
375
|
}
|
|
@@ -328,51 +403,137 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
328
403
|
const isAdvanced = targetProfile.toLowerCase() !== "general";
|
|
329
404
|
const license = checkLicenseStatus();
|
|
330
405
|
|
|
331
|
-
let prefix = "";
|
|
332
406
|
let finalProfile = targetProfile;
|
|
407
|
+
const extraBlocks = [];
|
|
408
|
+
|
|
409
|
+
sessionRequestCount++;
|
|
333
410
|
|
|
334
411
|
if (isAdvanced && !license.isPro) {
|
|
335
412
|
const reason = license.error ? ` (${license.error})` : "";
|
|
336
|
-
|
|
413
|
+
process.stderr.write(`⚠️ [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`);
|
|
337
414
|
finalProfile = "General";
|
|
415
|
+
extraBlocks.push(buildUpsellBlock(`Profile '${targetProfile}' requires PRO. Using 'General' as fallback.`));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const { processedText: processedContent2, wasTruncated: wasTruncated2 } = truncateIfFree(content, license.isPro);
|
|
419
|
+
if (wasTruncated2) extraBlocks.push(buildUpsellBlock(`File content was truncated to 50,000 characters (Free Tier limit).`));
|
|
420
|
+
|
|
421
|
+
const sanitized = performSanitization(processedContent2, finalProfile);
|
|
422
|
+
|
|
423
|
+
if (!license.isPro && extraBlocks.length === 0 && sessionRequestCount % UPSELL_EVERY_N === 0) {
|
|
424
|
+
extraBlocks.push(buildUpsellBlock(null));
|
|
338
425
|
}
|
|
339
426
|
|
|
340
|
-
const sanitized = performSanitization(content, finalProfile);
|
|
341
427
|
return {
|
|
342
428
|
content: [
|
|
343
|
-
{
|
|
344
|
-
|
|
345
|
-
text: `${prefix}${sanitized}`
|
|
346
|
-
}
|
|
429
|
+
{ type: "text", text: sanitized },
|
|
430
|
+
...extraBlocks
|
|
347
431
|
]
|
|
348
432
|
};
|
|
349
433
|
}
|
|
350
434
|
|
|
435
|
+
if (name === "check_status") {
|
|
436
|
+
const license = checkLicenseStatus();
|
|
437
|
+
const tier = license.isPro ? 'PRO' : 'FREE';
|
|
438
|
+
const tierIcon = license.isPro ? '✅' : '🔓';
|
|
439
|
+
const profileList = license.isPro
|
|
440
|
+
? 'All 23 profiles active (General, Dev, Medical, Legal, Finance, HR…)'
|
|
441
|
+
: 'General only — PRO unlocks 22 industry profiles';
|
|
442
|
+
const rulesStatus = license.isPro ? '✅ Active (privacyscrubber.json)' : '🔒 Locked — requires PRO';
|
|
443
|
+
const sizeLimit = license.isPro ? 'Unlimited' : '50,000 characters per request';
|
|
444
|
+
|
|
445
|
+
const lines = [
|
|
446
|
+
'╔══════════════════════════════════════════════════╗',
|
|
447
|
+
'║ PrivacyScrubber MCP Server v1.6.7 ║',
|
|
448
|
+
'╠══════════════════════════════════════════════════╣',
|
|
449
|
+
`║ ${tierIcon} Tier: ${tier.padEnd(43)}║`,
|
|
450
|
+
`║ 📊 Session requests: ${String(sessionRequestCount).padEnd(27)}║`,
|
|
451
|
+
`║ 📁 Input size limit: ${sizeLimit.padEnd(27)}║`,
|
|
452
|
+
'╠══════════════════════════════════════════════════╣',
|
|
453
|
+
`║ 🏷️ Profiles: ${profileList.substring(0,35).padEnd(35)}║`,
|
|
454
|
+
`║ 📋 Custom rules: ${rulesStatus.padEnd(31)}║`,
|
|
455
|
+
'╠══════════════════════════════════════════════════╣',
|
|
456
|
+
];
|
|
457
|
+
|
|
458
|
+
if (license.isPro) {
|
|
459
|
+
lines.push(
|
|
460
|
+
'║ ✅ PRO is active. All features unlocked. ║',
|
|
461
|
+
'║ To regenerate your key or manage billing: ║',
|
|
462
|
+
'║ https://privacyscrubber.com/pricing ║'
|
|
463
|
+
);
|
|
464
|
+
} else {
|
|
465
|
+
lines.push(
|
|
466
|
+
'║ 💳 Upgrade to PRO — $110 Lifetime ║',
|
|
467
|
+
'║ https://privacyscrubber.com/pricing ║',
|
|
468
|
+
'╠══════════════════════════════════════════════════╣',
|
|
469
|
+
'║ After purchase, add your key to MCP config: ║',
|
|
470
|
+
'║ ║',
|
|
471
|
+
'║ Claude Desktop / Cursor / Windsurf: ║',
|
|
472
|
+
'║ "PRIVACYSCRUBBER_KEY": "<your-key-here>" ║',
|
|
473
|
+
'║ ║',
|
|
474
|
+
'║ Full setup guide: ║',
|
|
475
|
+
'║ https://privacyscrubber.com/features/mcp/ ║'
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
lines.push('╚══════════════════════════════════════════════════╝');
|
|
480
|
+
|
|
481
|
+
return {
|
|
482
|
+
content: [{ type: "text", text: lines.join('\n') }]
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
351
486
|
return {
|
|
352
487
|
isError: true,
|
|
353
|
-
content: [
|
|
354
|
-
{
|
|
355
|
-
type: "text",
|
|
356
|
-
text: `Unknown tool: ${name}`
|
|
357
|
-
}
|
|
358
|
-
]
|
|
488
|
+
content: [{ type: "text", text: `Unknown tool: ${name}` }]
|
|
359
489
|
};
|
|
360
490
|
} catch (error) {
|
|
361
491
|
return {
|
|
362
492
|
isError: true,
|
|
363
|
-
content: [
|
|
364
|
-
{
|
|
365
|
-
type: "text",
|
|
366
|
-
text: `Error executing tool: ${error.message}`
|
|
367
|
-
}
|
|
368
|
-
]
|
|
493
|
+
content: [{ type: "text", text: `Error executing tool: ${error.message}` }]
|
|
369
494
|
};
|
|
370
495
|
}
|
|
371
496
|
});
|
|
372
497
|
|
|
498
|
+
function loadCustomRules() {
|
|
499
|
+
const license = checkLicenseStatus();
|
|
500
|
+
|
|
501
|
+
let configPath = path.resolve(process.cwd(), 'privacyscrubber.json');
|
|
502
|
+
|
|
503
|
+
if (!fs.existsSync(configPath)) {
|
|
504
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
|
505
|
+
if (homeDir) {
|
|
506
|
+
configPath = path.resolve(homeDir, 'privacyscrubber.json');
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (fs.existsSync(configPath)) {
|
|
511
|
+
try {
|
|
512
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
513
|
+
const rules = config.customRules || [];
|
|
514
|
+
if (rules.length > 0) {
|
|
515
|
+
if (license.isPro) {
|
|
516
|
+
return rules.map(r => ({
|
|
517
|
+
pattern: r.pattern || r.regex || "",
|
|
518
|
+
label: (r.label || r.category || "CUSTOM").toUpperCase(),
|
|
519
|
+
regex: r.pattern || r.regex || "",
|
|
520
|
+
category: (r.label || r.category || "CUSTOM").toUpperCase()
|
|
521
|
+
})).filter(r => r.pattern);
|
|
522
|
+
} else {
|
|
523
|
+
process.stderr.write("⚠️ [PrivacyScrubber] Custom rules detected in privacyscrubber.json, but are ignored in the Free Tier. Set PRIVACYSCRUBBER_KEY to your PRO license key.\n");
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
} catch (e) {
|
|
527
|
+
process.stderr.write(`Error parsing privacyscrubber.json: ${e.message}\n`);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return [];
|
|
531
|
+
}
|
|
532
|
+
|
|
373
533
|
// Run helper
|
|
374
534
|
function performSanitization(text, profile) {
|
|
375
|
-
const
|
|
535
|
+
const customRules = loadCustomRules();
|
|
536
|
+
const result = PrivacyScrubberCore.scrubText(text, customRules, {}, profile, sessionMap);
|
|
376
537
|
|
|
377
538
|
// Update our volatile map with new matches
|
|
378
539
|
if (result.tokenMap) {
|
|
@@ -384,6 +545,15 @@ function performSanitization(text, profile) {
|
|
|
384
545
|
return result.scrubbedText;
|
|
385
546
|
}
|
|
386
547
|
|
|
548
|
+
function truncateIfFree(text, isPro) {
|
|
549
|
+
const MAX_FREE_CHARS = 50000;
|
|
550
|
+
if (!isPro && text.length > MAX_FREE_CHARS) {
|
|
551
|
+
process.stderr.write(`⚠️ [PrivacyScrubber] Input truncated to ${MAX_FREE_CHARS} characters (Free Tier Limit). Set PRIVACYSCRUBBER_KEY to your PRO license key for unlimited size.\n`);
|
|
552
|
+
return { processedText: text.substring(0, MAX_FREE_CHARS), wasTruncated: true };
|
|
553
|
+
}
|
|
554
|
+
return { processedText: text, wasTruncated: false };
|
|
555
|
+
}
|
|
556
|
+
|
|
387
557
|
// Start the server transport
|
|
388
558
|
const transport = new StdioServerTransport();
|
|
389
559
|
server.connect(transport).catch((error) => {
|
package/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": "0.3",
|
|
3
3
|
"name": "privacyscrubber/pii-masking-mcp",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.6.6",
|
|
5
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
6
|
"author": {
|
|
7
7
|
"name": "PrivacyScrubber",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@privacyscrubber/mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.7",
|
|
4
4
|
"mcpName": "io.github.moxno/privacyscrubber-mcp",
|
|
5
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
6
|
"main": "index.js",
|