@privacyscrubber/mcp-server 1.0.0 → 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.
- package/CONTRIBUTING.md +52 -0
- package/LICENSE +21 -0
- package/README.md +108 -19
- package/SECURITY.md +30 -0
- package/index.js +125 -6
- package/llms.txt +37 -0
- package/manifest.json +28 -0
- package/package.json +17 -4
- package/scrubber-core.cjs +11 -1528
- package/server.json +29 -0
- package/smithery.yaml +20 -0
- package/test-mcp-deep.js +209 -0
- package/test-mcp.js +1 -1
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: ""
|
package/test-mcp-deep.js
ADDED
|
@@ -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, '
|
|
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...");
|