@streamblur/mcp 0.1.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,43 @@
1
+ # streamblur-mcp
2
+
3
+ StreamBlur MCP server that redacts API keys, tokens, credentials, and sensitive secrets from text and files.
4
+
5
+ ## Features
6
+
7
+ - 3 MCP tools:
8
+ - `redact_text`: Redact secrets in a string.
9
+ - `redact_file`: Read a file and return redacted content without writing changes.
10
+ - `scan_text`: Return detected secrets with `type`, `start`, and `end` positions.
11
+ - 50+ credential patterns including OpenAI, Anthropic, GitHub, AWS, Stripe, Twilio, Slack, SendGrid, database URLs, bearer tokens, private keys, and generic `.env` assignments.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install
17
+ npm run build
18
+ ```
19
+
20
+ ## Run
21
+
22
+ ```bash
23
+ streamblur-mcp
24
+ ```
25
+
26
+ or
27
+
28
+ ```bash
29
+ node dist/index.js
30
+ ```
31
+
32
+ ## Development
33
+
34
+ ```bash
35
+ npm run build
36
+ node dist/test/redact.test.js
37
+ ```
38
+
39
+ ## Quick manual redaction check
40
+
41
+ ```bash
42
+ node -e "const r = require('./dist/redact'); const text = require('fs').readFileSync('./test/sample-secrets.txt','utf8'); console.log('BEFORE:\n' + text); console.log('\nAFTER:\n' + r.redactText(text));"
43
+ ```
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const node_fs_1 = require("node:fs");
5
+ const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
6
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
7
+ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
8
+ const redact_1 = require("./redact");
9
+ const server = new index_js_1.Server({
10
+ name: "streamblur-mcp",
11
+ version: "0.1.0"
12
+ }, {
13
+ capabilities: {
14
+ tools: {}
15
+ }
16
+ });
17
+ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
18
+ return {
19
+ tools: [
20
+ {
21
+ name: "redact_text",
22
+ description: "Redacts secrets from input text and replaces them with [REDACTED:type]",
23
+ inputSchema: {
24
+ type: "object",
25
+ properties: {
26
+ text: {
27
+ type: "string",
28
+ description: "Text to redact"
29
+ }
30
+ },
31
+ required: ["text"],
32
+ additionalProperties: false
33
+ }
34
+ },
35
+ {
36
+ name: "redact_file",
37
+ description: "Reads a file and returns redacted content without modifying the original file",
38
+ inputSchema: {
39
+ type: "object",
40
+ properties: {
41
+ path: {
42
+ type: "string",
43
+ description: "Path to file"
44
+ }
45
+ },
46
+ required: ["path"],
47
+ additionalProperties: false
48
+ }
49
+ },
50
+ {
51
+ name: "scan_text",
52
+ description: "Scans text and returns a list of detected secrets with type and position",
53
+ inputSchema: {
54
+ type: "object",
55
+ properties: {
56
+ text: {
57
+ type: "string",
58
+ description: "Text to scan"
59
+ }
60
+ },
61
+ required: ["text"],
62
+ additionalProperties: false
63
+ }
64
+ }
65
+ ]
66
+ };
67
+ });
68
+ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
69
+ const args = request.params.arguments ?? {};
70
+ switch (request.params.name) {
71
+ case "redact_text": {
72
+ const text = args.text;
73
+ if (typeof text !== "string") {
74
+ throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, "'text' must be a string");
75
+ }
76
+ return {
77
+ content: [{ type: "text", text: (0, redact_1.redactText)(text) }]
78
+ };
79
+ }
80
+ case "redact_file": {
81
+ const path = args.path;
82
+ if (typeof path !== "string") {
83
+ throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, "'path' must be a string");
84
+ }
85
+ const content = (0, node_fs_1.readFileSync)(path, "utf8");
86
+ return {
87
+ content: [{ type: "text", text: (0, redact_1.redactText)(content) }]
88
+ };
89
+ }
90
+ case "scan_text": {
91
+ const text = args.text;
92
+ if (typeof text !== "string") {
93
+ throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, "'text' must be a string");
94
+ }
95
+ const detections = (0, redact_1.scanText)(text).map((detection) => ({
96
+ type: detection.type,
97
+ start: detection.start,
98
+ end: detection.end
99
+ }));
100
+ return {
101
+ content: [{ type: "text", text: JSON.stringify(detections, null, 2) }]
102
+ };
103
+ }
104
+ default:
105
+ throw new types_js_1.McpError(types_js_1.ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
106
+ }
107
+ });
108
+ async function main() {
109
+ const transport = new stdio_js_1.StdioServerTransport();
110
+ await server.connect(transport);
111
+ }
112
+ main().catch((error) => {
113
+ console.error("streamblur-mcp failed to start", error);
114
+ process.exit(1);
115
+ });
116
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ export type CredentialPattern = {
2
+ type: string;
3
+ regex: RegExp;
4
+ };
5
+ export declare const credentialPatterns: CredentialPattern[];
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.credentialPatterns = void 0;
4
+ exports.credentialPatterns = [
5
+ { type: "anthropic_api_key", regex: /sk-ant-api03-[A-Za-z0-9_-]{20,120}/g },
6
+ { type: "openai_project_key", regex: /sk-proj-[A-Za-z0-9_-]{20,120}/g },
7
+ { type: "openai_api_key", regex: /\bsk-[A-Za-z0-9]{20,120}\b/g },
8
+ { type: "github_pat", regex: /github_pat_[A-Za-z0-9_]{20,255}/g },
9
+ { type: "github_token_ghp", regex: /\bghp_[A-Za-z0-9]{20,255}\b/g },
10
+ { type: "github_token_gho", regex: /\bgho_[A-Za-z0-9]{20,255}\b/g },
11
+ { type: "github_token_ghs", regex: /\bghs_[A-Za-z0-9]{20,255}\b/g },
12
+ { type: "aws_access_key", regex: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g },
13
+ { type: "aws_secret_key", regex: /\b(?:AWS_SECRET_ACCESS_KEY|aws_secret_access_key)\s*[:=]\s*['\"]?[A-Za-z0-9\/+=]{40}['\"]?/g },
14
+ { type: "aws_secret_key_raw", regex: /\b[A-Za-z0-9\/+=]{40}\b/g },
15
+ { type: "google_api_key", regex: /\bAIza[0-9A-Za-z_-]{35}\b/g },
16
+ { type: "stripe_secret_live", regex: /\bsk_live_[A-Za-z0-9]{16,120}\b/g },
17
+ { type: "stripe_publishable_live", regex: /\bpk_live_[A-Za-z0-9]{16,120}\b/g },
18
+ { type: "stripe_secret_test", regex: /\bsk_test_[A-Za-z0-9]{16,120}\b/g },
19
+ { type: "stripe_restricted_live", regex: /\brk_live_[A-Za-z0-9]{16,120}\b/g },
20
+ { type: "twilio_sid", regex: /\bAC[a-f0-9]{32}\b/gi },
21
+ { type: "twilio_api_key", regex: /\bSK[a-f0-9]{32}\b/gi },
22
+ { type: "twilio_auth_token", regex: /\b(?:TWILIO_AUTH_TOKEN|twilio_auth_token)\s*[:=]\s*['\"]?[A-Fa-f0-9]{32}['\"]?/g },
23
+ { type: "sendgrid_api_key", regex: /\bSG\.[A-Za-z0-9_-]{20,120}\b/g },
24
+ { type: "slack_bot_token", regex: /\bxoxb-[0-9A-Za-z-]{20,120}\b/g },
25
+ { type: "slack_user_token", regex: /\bxoxp-[0-9A-Za-z-]{20,120}\b/g },
26
+ { type: "slack_session_token", regex: /\bxoxs-[0-9A-Za-z-]{20,120}\b/g },
27
+ { type: "discord_bot_token", regex: /\b[A-Za-z\d_-]{24}\.[A-Za-z\d_-]{6}\.[A-Za-z\d_-]{27}\b/g },
28
+ { type: "jwt_token", regex: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g },
29
+ { type: "postgres_url", regex: /\bpostgres(?:ql)?:\/\/[^\s'"`]+:[^\s'"`]+@[^\s'"`]+\b/g },
30
+ { type: "mysql_url", regex: /\bmysql:\/\/[^\s'"`]+:[^\s'"`]+@[^\s'"`]+\b/g },
31
+ { type: "mongodb_url", regex: /\bmongodb(?:\+srv)?:\/\/[^\s'"`]+:[^\s'"`]+@[^\s'"`]+\b/g },
32
+ { type: "redis_url", regex: /\bredis(?:s)?:\/\/[^\s'"`]*:[^\s'"`]+@[^\s'"`]+\b/g },
33
+ { type: "http_basic_auth_url", regex: /\bhttps?:\/\/[^\s:@\/]+:[^\s@\/]+@[^\s'"`]+\b/g },
34
+ { type: "rsa_private_key", regex: /-----BEGIN RSA PRIVATE KEY-----[\s\S]+?-----END RSA PRIVATE KEY-----/g },
35
+ { type: "ec_private_key", regex: /-----BEGIN EC PRIVATE KEY-----[\s\S]+?-----END EC PRIVATE KEY-----/g },
36
+ { type: "generic_private_key", regex: /-----BEGIN PRIVATE KEY-----[\s\S]+?-----END PRIVATE KEY-----/g },
37
+ { type: "openssh_private_key", regex: /-----BEGIN OPENSSH PRIVATE KEY-----[\s\S]+?-----END OPENSSH PRIVATE KEY-----/g },
38
+ { type: "dsa_private_key", regex: /-----BEGIN DSA PRIVATE KEY-----[\s\S]+?-----END DSA PRIVATE KEY-----/g },
39
+ { type: "authorization_bearer", regex: /Authorization\s*:\s*Bearer\s+[A-Za-z0-9._-]{12,}/gi },
40
+ { type: "firebase_web_api_key", regex: /\bAIza[0-9A-Za-z_-]{35}\b/g },
41
+ { type: "firebase_db_url", regex: /\bhttps:\/\/[a-z0-9-]+\.firebaseio\.com\b/gi },
42
+ { type: "firebase_service_account", regex: /"type"\s*:\s*"service_account"[\s\S]*?"private_key"\s*:\s*"-----BEGIN PRIVATE KEY-----[\s\S]*?-----END PRIVATE KEY-----\\n"/g },
43
+ { type: "netlify_token", regex: /\bnfp_[A-Za-z0-9]{20,120}\b/g },
44
+ { type: "vercel_token", regex: /\b(?:vercel|VERCEL)_[A-Za-z0-9_]*TOKEN\s*[:=]\s*['\"]?[A-Za-z0-9]{20,120}['\"]?/g },
45
+ { type: "vercel_pat", regex: /\b[A-Za-z0-9]{24}_[A-Za-z0-9]{24}\b/g },
46
+ { type: "env_api_key", regex: /\bAPI_KEY\s*[:=]\s*['\"]?[^\s'\"]{8,}['\"]?/g },
47
+ { type: "env_openai_api_key", regex: /\bOPENAI_API_KEY\s*[:=]\s*['\"]?[^\s'\"]+['\"]?/g },
48
+ { type: "env_anthropic_api_key", regex: /\bANTHROPIC_API_KEY\s*[:=]\s*['\"]?[^\s'\"]+['\"]?/g },
49
+ { type: "env_github_token", regex: /\bGITHUB_TOKEN\s*[:=]\s*['\"]?[^\s'\"]+['\"]?/g },
50
+ { type: "env_secret_key", regex: /\bSECRET_KEY\s*[:=]\s*['\"]?[^\s'\"]{8,}['\"]?/g },
51
+ { type: "env_access_token", regex: /\bACCESS_TOKEN\s*[:=]\s*['\"]?[^\s'\"]{8,}['\"]?/g },
52
+ { type: "env_password", regex: /\bPASSWORD\s*[:=]\s*['\"]?[^\s'\"]{6,}['\"]?/g },
53
+ { type: "env_db_password", regex: /\bDB_PASSWORD\s*[:=]\s*['\"]?[^\s'\"]{6,}['\"]?/g },
54
+ { type: "env_database_url", regex: /\bDATABASE_URL\s*[:=]\s*['\"]?[^\s'\"]+['\"]?/g },
55
+ { type: "env_redis_url", regex: /\bREDIS_URL\s*[:=]\s*['\"]?[^\s'\"]+['\"]?/g },
56
+ { type: "env_sendgrid_api_key", regex: /\bSENDGRID_API_KEY\s*[:=]\s*['\"]?[^\s'\"]+['\"]?/g },
57
+ { type: "env_slack_token", regex: /\bSLACK(?:_BOT)?_TOKEN\s*[:=]\s*['\"]?[^\s'\"]+['\"]?/g },
58
+ { type: "env_stripe_key", regex: /\bSTRIPE(?:_SECRET)?_KEY\s*[:=]\s*['\"]?[^\s'\"]+['\"]?/g },
59
+ { type: "env_aws_access_key_id", regex: /\bAWS_ACCESS_KEY_ID\s*[:=]\s*['\"]?[A-Z0-9]{16,32}['\"]?/g },
60
+ { type: "env_aws_secret_access_key", regex: /\bAWS_SECRET_ACCESS_KEY\s*[:=]\s*['\"]?[A-Za-z0-9\/+=]{20,80}['\"]?/g },
61
+ { type: "env_twilio_auth_token", regex: /\bTWILIO_AUTH_TOKEN\s*[:=]\s*['\"]?[A-Fa-f0-9]{32}['\"]?/g },
62
+ { type: "env_private_key", regex: /\bPRIVATE_KEY\s*[:=]\s*['\"]?-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]+?-----END [A-Z ]*PRIVATE KEY-----['\"]?/g },
63
+ { type: "generic_token_assignment", regex: /\b(?:token|secret|passwd|password|api[_-]?key|client[_-]?secret)\b\s*[:=]\s*['\"]?[A-Za-z0-9._\-\/+=]{8,}['\"]?/gi },
64
+ { type: "npm_token", regex: /\bnpm_[A-Za-z0-9]{20,120}\b/g },
65
+ { type: "github_fine_grained_token", regex: /\bghu_[A-Za-z0-9]{20,255}\b/g },
66
+ { type: "azure_storage_key", regex: /\bDefaultEndpointsProtocol=https;AccountName=[^;]+;AccountKey=[A-Za-z0-9+/=]{40,};EndpointSuffix=core\.windows\.net\b/g },
67
+ { type: "gcp_service_account_json_key", regex: /"private_key_id"\s*:\s*"[a-f0-9]{30,}"/g },
68
+ { type: "oauth_refresh_token", regex: /\b1\/\/[A-Za-z0-9_-]{20,}\b/g },
69
+ { type: "mailgun_api_key", regex: /\bkey-[A-Za-z0-9]{32}\b/g },
70
+ { type: "shopify_access_token", regex: /\bshpat_[A-Za-z0-9]{20,120}\b/g },
71
+ { type: "gitlab_token", regex: /\bglpat-[A-Za-z0-9_-]{20,120}\b/g }
72
+ ];
73
+ //# sourceMappingURL=patterns.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"patterns.js","sourceRoot":"","sources":["../../src/patterns.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ export type SecretDetection = {
2
+ type: string;
3
+ value: string;
4
+ start: number;
5
+ end: number;
6
+ };
7
+ export declare function scanText(text: string): SecretDetection[];
8
+ export declare function redactText(text: string): string;
9
+ export declare function redactFile(filePath: string): string;
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.scanText = scanText;
4
+ exports.redactText = redactText;
5
+ exports.redactFile = redactFile;
6
+ const node_fs_1 = require("node:fs");
7
+ const patterns_1 = require("./patterns");
8
+ function runPatternScan(text) {
9
+ const hits = [];
10
+ for (const pattern of patterns_1.credentialPatterns) {
11
+ const regex = new RegExp(pattern.regex.source, pattern.regex.flags);
12
+ regex.lastIndex = 0;
13
+ let match;
14
+ while ((match = regex.exec(text)) !== null) {
15
+ const value = match[0];
16
+ const start = match.index;
17
+ const end = start + value.length;
18
+ hits.push({ type: pattern.type, value, start, end });
19
+ if (value.length === 0) {
20
+ regex.lastIndex += 1;
21
+ }
22
+ }
23
+ }
24
+ return hits;
25
+ }
26
+ function collapseOverlaps(detections) {
27
+ const sorted = [...detections].sort((a, b) => {
28
+ if (a.start !== b.start) {
29
+ return a.start - b.start;
30
+ }
31
+ return b.end - a.end;
32
+ });
33
+ const accepted = [];
34
+ for (const detection of sorted) {
35
+ const previous = accepted[accepted.length - 1];
36
+ if (!previous || detection.start >= previous.end) {
37
+ accepted.push(detection);
38
+ }
39
+ }
40
+ return accepted;
41
+ }
42
+ function scanText(text) {
43
+ return collapseOverlaps(runPatternScan(text));
44
+ }
45
+ function redactText(text) {
46
+ const detections = scanText(text);
47
+ if (detections.length === 0) {
48
+ return text;
49
+ }
50
+ let result = "";
51
+ let cursor = 0;
52
+ for (const detection of detections) {
53
+ result += text.slice(cursor, detection.start);
54
+ result += `[REDACTED:${detection.type}]`;
55
+ cursor = detection.end;
56
+ }
57
+ result += text.slice(cursor);
58
+ return result;
59
+ }
60
+ function redactFile(filePath) {
61
+ const content = (0, node_fs_1.readFileSync)(filePath, "utf8");
62
+ return redactText(content);
63
+ }
64
+ //# sourceMappingURL=redact.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redact.js","sourceRoot":"","sources":["../../src/redact.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const node_assert_1 = require("node:assert");
4
+ const node_fs_1 = require("node:fs");
5
+ const node_path_1 = require("node:path");
6
+ const redact_1 = require("../src/redact");
7
+ function run() {
8
+ const fixturePath = (0, node_path_1.join)(__dirname, "..", "test", "sample-secrets.txt");
9
+ const fixture = (0, node_fs_1.readFileSync)(fixturePath, "utf8");
10
+ const detections = (0, redact_1.scanText)(fixture);
11
+ node_assert_1.strict.ok(detections.length >= 10, "should detect multiple secrets");
12
+ const redacted = (0, redact_1.redactText)(fixture);
13
+ node_assert_1.strict.ok(redacted.includes("[REDACTED:openai_project_key]"));
14
+ node_assert_1.strict.ok(redacted.includes("[REDACTED:anthropic_api_key]"));
15
+ node_assert_1.strict.ok(redacted.includes("[REDACTED:github_token_ghp]"));
16
+ node_assert_1.strict.ok(redacted.includes("[REDACTED:aws_access_key]"));
17
+ node_assert_1.strict.ok(redacted.includes("[REDACTED:sendgrid_api_key]"));
18
+ const fromFile = (0, redact_1.redactFile)(fixturePath);
19
+ node_assert_1.strict.equal(fromFile, redacted, "redactFile should match redactText output");
20
+ const codeFixturePath = (0, node_path_1.join)(__dirname, "..", "test", "sample-code.ts");
21
+ const codeFixture = (0, node_fs_1.readFileSync)(codeFixturePath, "utf8");
22
+ const codeRedacted = (0, redact_1.redactText)(codeFixture);
23
+ node_assert_1.strict.ok(codeRedacted.includes("[REDACTED:openai_project_key]"));
24
+ node_assert_1.strict.ok(codeRedacted.includes("[REDACTED:github_token_ghp]"));
25
+ node_assert_1.strict.ok(codeRedacted.includes("[REDACTED:postgres_url]"));
26
+ console.log("All redact tests passed.");
27
+ }
28
+ run();
29
+ //# sourceMappingURL=redact.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redact.test.js","sourceRoot":"","sources":["../../test/redact.test.ts"],"names":[],"mappings":";;AAAA,6CAA+C;AAC/C,qCAAuC;AACvC,yCAAiC;AACjC,0CAAiE;AAEjE,SAAS,GAAG;IACV,MAAM,WAAW,GAAG,IAAA,gBAAI,EAAC,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,oBAAoB,CAAC,CAAC;IACxE,MAAM,OAAO,GAAG,IAAA,sBAAY,EAAC,WAAW,EAAE,MAAM,CAAC,CAAC;IAElD,MAAM,UAAU,GAAG,IAAA,iBAAQ,EAAC,OAAO,CAAC,CAAC;IACrC,oBAAM,CAAC,EAAE,CAAC,UAAU,CAAC,MAAM,IAAI,EAAE,EAAE,gCAAgC,CAAC,CAAC;IAErE,MAAM,QAAQ,GAAG,IAAA,mBAAU,EAAC,OAAO,CAAC,CAAC;IACrC,oBAAM,CAAC,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,+BAA+B,CAAC,CAAC,CAAC;IAC9D,oBAAM,CAAC,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,8BAA8B,CAAC,CAAC,CAAC;IAC7D,oBAAM,CAAC,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,6BAA6B,CAAC,CAAC,CAAC;IAC5D,oBAAM,CAAC,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,2BAA2B,CAAC,CAAC,CAAC;IAC1D,oBAAM,CAAC,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,6BAA6B,CAAC,CAAC,CAAC;IAE5D,MAAM,QAAQ,GAAG,IAAA,mBAAU,EAAC,WAAW,CAAC,CAAC;IACzC,oBAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,QAAQ,EAAE,2CAA2C,CAAC,CAAC;IAE9E,MAAM,eAAe,GAAG,IAAA,gBAAI,EAAC,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,gBAAgB,CAAC,CAAC;IACxE,MAAM,WAAW,GAAG,IAAA,sBAAY,EAAC,eAAe,EAAE,MAAM,CAAC,CAAC;IAC1D,MAAM,YAAY,GAAG,IAAA,mBAAU,EAAC,WAAW,CAAC,CAAC;IAC7C,oBAAM,CAAC,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,+BAA+B,CAAC,CAAC,CAAC;IAClE,oBAAM,CAAC,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,6BAA6B,CAAC,CAAC,CAAC;IAChE,oBAAM,CAAC,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,yBAAyB,CAAC,CAAC,CAAC;IAE5D,OAAO,CAAC,GAAG,CAAC,0BAA0B,CAAC,CAAC;AAC1C,CAAC;AAED,GAAG,EAAE,CAAC"}
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@streamblur/mcp",
3
+ "version": "0.1.0",
4
+ "description": "StreamBlur MCP server - redact API keys, tokens, and secrets from text and files",
5
+ "main": "dist/src/index.js",
6
+ "bin": {
7
+ "streamblur-mcp": "./dist/src/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc -p tsconfig.json",
11
+ "start": "node dist/src/index.js",
12
+ "test": "node dist/test/redact.test.js"
13
+ },
14
+ "keywords": ["mcp", "security", "redact", "api-keys", "secrets", "privacy", "streamblur"],
15
+ "author": "StreamBlur",
16
+ "license": "MIT",
17
+ "dependencies": {
18
+ "@modelcontextprotocol/sdk": "^1.0.0"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^22.0.0",
22
+ "typescript": "^5.0.0"
23
+ }
24
+ }
package/src/index.ts ADDED
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync } from "node:fs";
4
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import {
7
+ CallToolRequestSchema,
8
+ ErrorCode,
9
+ ListToolsRequestSchema,
10
+ McpError
11
+ } from "@modelcontextprotocol/sdk/types.js";
12
+ import { redactText, scanText } from "./redact";
13
+
14
+ const server = new Server(
15
+ {
16
+ name: "streamblur-mcp",
17
+ version: "0.1.0"
18
+ },
19
+ {
20
+ capabilities: {
21
+ tools: {}
22
+ }
23
+ }
24
+ );
25
+
26
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
27
+ return {
28
+ tools: [
29
+ {
30
+ name: "redact_text",
31
+ description: "Redacts secrets from input text and replaces them with [REDACTED:type]",
32
+ inputSchema: {
33
+ type: "object",
34
+ properties: {
35
+ text: {
36
+ type: "string",
37
+ description: "Text to redact"
38
+ }
39
+ },
40
+ required: ["text"],
41
+ additionalProperties: false
42
+ }
43
+ },
44
+ {
45
+ name: "redact_file",
46
+ description: "Reads a file and returns redacted content without modifying the original file",
47
+ inputSchema: {
48
+ type: "object",
49
+ properties: {
50
+ path: {
51
+ type: "string",
52
+ description: "Path to file"
53
+ }
54
+ },
55
+ required: ["path"],
56
+ additionalProperties: false
57
+ }
58
+ },
59
+ {
60
+ name: "scan_text",
61
+ description: "Scans text and returns a list of detected secrets with type and position",
62
+ inputSchema: {
63
+ type: "object",
64
+ properties: {
65
+ text: {
66
+ type: "string",
67
+ description: "Text to scan"
68
+ }
69
+ },
70
+ required: ["text"],
71
+ additionalProperties: false
72
+ }
73
+ }
74
+ ]
75
+ };
76
+ });
77
+
78
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
79
+ const args = request.params.arguments ?? {};
80
+
81
+ switch (request.params.name) {
82
+ case "redact_text": {
83
+ const text = args.text;
84
+ if (typeof text !== "string") {
85
+ throw new McpError(ErrorCode.InvalidParams, "'text' must be a string");
86
+ }
87
+
88
+ return {
89
+ content: [{ type: "text", text: redactText(text) }]
90
+ };
91
+ }
92
+
93
+ case "redact_file": {
94
+ const path = args.path;
95
+ if (typeof path !== "string") {
96
+ throw new McpError(ErrorCode.InvalidParams, "'path' must be a string");
97
+ }
98
+
99
+ const content = readFileSync(path, "utf8");
100
+ return {
101
+ content: [{ type: "text", text: redactText(content) }]
102
+ };
103
+ }
104
+
105
+ case "scan_text": {
106
+ const text = args.text;
107
+ if (typeof text !== "string") {
108
+ throw new McpError(ErrorCode.InvalidParams, "'text' must be a string");
109
+ }
110
+
111
+ const detections = scanText(text).map((detection) => ({
112
+ type: detection.type,
113
+ start: detection.start,
114
+ end: detection.end
115
+ }));
116
+
117
+ return {
118
+ content: [{ type: "text", text: JSON.stringify(detections, null, 2) }]
119
+ };
120
+ }
121
+
122
+ default:
123
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
124
+ }
125
+ });
126
+
127
+ async function main(): Promise<void> {
128
+ const transport = new StdioServerTransport();
129
+ await server.connect(transport);
130
+ }
131
+
132
+ main().catch((error) => {
133
+ console.error("streamblur-mcp failed to start", error);
134
+ process.exit(1);
135
+ });
@@ -0,0 +1,91 @@
1
+ export type CredentialPattern = {
2
+ type: string;
3
+ regex: RegExp;
4
+ };
5
+
6
+ export const credentialPatterns: CredentialPattern[] = [
7
+ { type: "anthropic_api_key", regex: /sk-ant-api03-[A-Za-z0-9_-]{20,120}/g },
8
+ { type: "openai_project_key", regex: /sk-proj-[A-Za-z0-9_-]{20,120}/g },
9
+ { type: "openai_api_key", regex: /\bsk-[A-Za-z0-9]{20,120}\b/g },
10
+
11
+ { type: "github_pat", regex: /github_pat_[A-Za-z0-9_]{20,255}/g },
12
+ { type: "github_token_ghp", regex: /\bghp_[A-Za-z0-9]{20,255}\b/g },
13
+ { type: "github_token_gho", regex: /\bgho_[A-Za-z0-9]{20,255}\b/g },
14
+ { type: "github_token_ghs", regex: /\bghs_[A-Za-z0-9]{20,255}\b/g },
15
+
16
+ { type: "aws_access_key", regex: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g },
17
+ { type: "aws_secret_key", regex: /\b(?:AWS_SECRET_ACCESS_KEY|aws_secret_access_key)\s*[:=]\s*['\"]?[A-Za-z0-9\/+=]{40}['\"]?/g },
18
+ { type: "aws_secret_key_raw", regex: /\b[A-Za-z0-9\/+=]{40}\b/g },
19
+
20
+ { type: "google_api_key", regex: /\bAIza[0-9A-Za-z_-]{35}\b/g },
21
+
22
+ { type: "stripe_secret_live", regex: /\bsk_live_[A-Za-z0-9]{16,120}\b/g },
23
+ { type: "stripe_publishable_live", regex: /\bpk_live_[A-Za-z0-9]{16,120}\b/g },
24
+ { type: "stripe_secret_test", regex: /\bsk_test_[A-Za-z0-9]{16,120}\b/g },
25
+ { type: "stripe_restricted_live", regex: /\brk_live_[A-Za-z0-9]{16,120}\b/g },
26
+
27
+ { type: "twilio_sid", regex: /\bAC[a-f0-9]{32}\b/gi },
28
+ { type: "twilio_api_key", regex: /\bSK[a-f0-9]{32}\b/gi },
29
+ { type: "twilio_auth_token", regex: /\b(?:TWILIO_AUTH_TOKEN|twilio_auth_token)\s*[:=]\s*['\"]?[A-Fa-f0-9]{32}['\"]?/g },
30
+
31
+ { type: "sendgrid_api_key", regex: /\bSG\.[A-Za-z0-9_-]{20,120}\b/g },
32
+
33
+ { type: "slack_bot_token", regex: /\bxoxb-[0-9A-Za-z-]{20,120}\b/g },
34
+ { type: "slack_user_token", regex: /\bxoxp-[0-9A-Za-z-]{20,120}\b/g },
35
+ { type: "slack_session_token", regex: /\bxoxs-[0-9A-Za-z-]{20,120}\b/g },
36
+
37
+ { type: "discord_bot_token", regex: /\b[A-Za-z\d_-]{24}\.[A-Za-z\d_-]{6}\.[A-Za-z\d_-]{27}\b/g },
38
+
39
+ { type: "jwt_token", regex: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g },
40
+
41
+ { type: "postgres_url", regex: /\bpostgres(?:ql)?:\/\/[^\s'"`]+:[^\s'"`]+@[^\s'"`]+\b/g },
42
+ { type: "mysql_url", regex: /\bmysql:\/\/[^\s'"`]+:[^\s'"`]+@[^\s'"`]+\b/g },
43
+ { type: "mongodb_url", regex: /\bmongodb(?:\+srv)?:\/\/[^\s'"`]+:[^\s'"`]+@[^\s'"`]+\b/g },
44
+ { type: "redis_url", regex: /\bredis(?:s)?:\/\/[^\s'"`]*:[^\s'"`]+@[^\s'"`]+\b/g },
45
+
46
+ { type: "http_basic_auth_url", regex: /\bhttps?:\/\/[^\s:@\/]+:[^\s@\/]+@[^\s'"`]+\b/g },
47
+
48
+ { type: "rsa_private_key", regex: /-----BEGIN RSA PRIVATE KEY-----[\s\S]+?-----END RSA PRIVATE KEY-----/g },
49
+ { type: "ec_private_key", regex: /-----BEGIN EC PRIVATE KEY-----[\s\S]+?-----END EC PRIVATE KEY-----/g },
50
+ { type: "generic_private_key", regex: /-----BEGIN PRIVATE KEY-----[\s\S]+?-----END PRIVATE KEY-----/g },
51
+ { type: "openssh_private_key", regex: /-----BEGIN OPENSSH PRIVATE KEY-----[\s\S]+?-----END OPENSSH PRIVATE KEY-----/g },
52
+ { type: "dsa_private_key", regex: /-----BEGIN DSA PRIVATE KEY-----[\s\S]+?-----END DSA PRIVATE KEY-----/g },
53
+
54
+ { type: "authorization_bearer", regex: /Authorization\s*:\s*Bearer\s+[A-Za-z0-9._-]{12,}/gi },
55
+
56
+ { type: "firebase_web_api_key", regex: /\bAIza[0-9A-Za-z_-]{35}\b/g },
57
+ { type: "firebase_db_url", regex: /\bhttps:\/\/[a-z0-9-]+\.firebaseio\.com\b/gi },
58
+ { type: "firebase_service_account", regex: /"type"\s*:\s*"service_account"[\s\S]*?"private_key"\s*:\s*"-----BEGIN PRIVATE KEY-----[\s\S]*?-----END PRIVATE KEY-----\\n"/g },
59
+
60
+ { type: "netlify_token", regex: /\bnfp_[A-Za-z0-9]{20,120}\b/g },
61
+ { type: "vercel_token", regex: /\b(?:vercel|VERCEL)_[A-Za-z0-9_]*TOKEN\s*[:=]\s*['\"]?[A-Za-z0-9]{20,120}['\"]?/g },
62
+ { type: "vercel_pat", regex: /\b[A-Za-z0-9]{24}_[A-Za-z0-9]{24}\b/g },
63
+
64
+ { type: "env_api_key", regex: /\bAPI_KEY\s*[:=]\s*['\"]?[^\s'\"]{8,}['\"]?/g },
65
+ { type: "env_openai_api_key", regex: /\bOPENAI_API_KEY\s*[:=]\s*['\"]?[^\s'\"]+['\"]?/g },
66
+ { type: "env_anthropic_api_key", regex: /\bANTHROPIC_API_KEY\s*[:=]\s*['\"]?[^\s'\"]+['\"]?/g },
67
+ { type: "env_github_token", regex: /\bGITHUB_TOKEN\s*[:=]\s*['\"]?[^\s'\"]+['\"]?/g },
68
+ { type: "env_secret_key", regex: /\bSECRET_KEY\s*[:=]\s*['\"]?[^\s'\"]{8,}['\"]?/g },
69
+ { type: "env_access_token", regex: /\bACCESS_TOKEN\s*[:=]\s*['\"]?[^\s'\"]{8,}['\"]?/g },
70
+ { type: "env_password", regex: /\bPASSWORD\s*[:=]\s*['\"]?[^\s'\"]{6,}['\"]?/g },
71
+ { type: "env_db_password", regex: /\bDB_PASSWORD\s*[:=]\s*['\"]?[^\s'\"]{6,}['\"]?/g },
72
+ { type: "env_database_url", regex: /\bDATABASE_URL\s*[:=]\s*['\"]?[^\s'\"]+['\"]?/g },
73
+ { type: "env_redis_url", regex: /\bREDIS_URL\s*[:=]\s*['\"]?[^\s'\"]+['\"]?/g },
74
+ { type: "env_sendgrid_api_key", regex: /\bSENDGRID_API_KEY\s*[:=]\s*['\"]?[^\s'\"]+['\"]?/g },
75
+ { type: "env_slack_token", regex: /\bSLACK(?:_BOT)?_TOKEN\s*[:=]\s*['\"]?[^\s'\"]+['\"]?/g },
76
+ { type: "env_stripe_key", regex: /\bSTRIPE(?:_SECRET)?_KEY\s*[:=]\s*['\"]?[^\s'\"]+['\"]?/g },
77
+ { type: "env_aws_access_key_id", regex: /\bAWS_ACCESS_KEY_ID\s*[:=]\s*['\"]?[A-Z0-9]{16,32}['\"]?/g },
78
+ { type: "env_aws_secret_access_key", regex: /\bAWS_SECRET_ACCESS_KEY\s*[:=]\s*['\"]?[A-Za-z0-9\/+=]{20,80}['\"]?/g },
79
+ { type: "env_twilio_auth_token", regex: /\bTWILIO_AUTH_TOKEN\s*[:=]\s*['\"]?[A-Fa-f0-9]{32}['\"]?/g },
80
+ { type: "env_private_key", regex: /\bPRIVATE_KEY\s*[:=]\s*['\"]?-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]+?-----END [A-Z ]*PRIVATE KEY-----['\"]?/g },
81
+
82
+ { type: "generic_token_assignment", regex: /\b(?:token|secret|passwd|password|api[_-]?key|client[_-]?secret)\b\s*[:=]\s*['\"]?[A-Za-z0-9._\-\/+=]{8,}['\"]?/gi },
83
+ { type: "npm_token", regex: /\bnpm_[A-Za-z0-9]{20,120}\b/g },
84
+ { type: "github_fine_grained_token", regex: /\bghu_[A-Za-z0-9]{20,255}\b/g },
85
+ { type: "azure_storage_key", regex: /\bDefaultEndpointsProtocol=https;AccountName=[^;]+;AccountKey=[A-Za-z0-9+/=]{40,};EndpointSuffix=core\.windows\.net\b/g },
86
+ { type: "gcp_service_account_json_key", regex: /"private_key_id"\s*:\s*"[a-f0-9]{30,}"/g },
87
+ { type: "oauth_refresh_token", regex: /\b1\/\/[A-Za-z0-9_-]{20,}\b/g },
88
+ { type: "mailgun_api_key", regex: /\bkey-[A-Za-z0-9]{32}\b/g },
89
+ { type: "shopify_access_token", regex: /\bshpat_[A-Za-z0-9]{20,120}\b/g },
90
+ { type: "gitlab_token", regex: /\bglpat-[A-Za-z0-9_-]{20,120}\b/g }
91
+ ];
package/src/redact.ts ADDED
@@ -0,0 +1,79 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { credentialPatterns } from "./patterns";
3
+
4
+ export type SecretDetection = {
5
+ type: string;
6
+ value: string;
7
+ start: number;
8
+ end: number;
9
+ };
10
+
11
+ function runPatternScan(text: string): SecretDetection[] {
12
+ const hits: SecretDetection[] = [];
13
+
14
+ for (const pattern of credentialPatterns) {
15
+ const regex = new RegExp(pattern.regex.source, pattern.regex.flags);
16
+ regex.lastIndex = 0;
17
+
18
+ let match: RegExpExecArray | null;
19
+ while ((match = regex.exec(text)) !== null) {
20
+ const value = match[0];
21
+ const start = match.index;
22
+ const end = start + value.length;
23
+ hits.push({ type: pattern.type, value, start, end });
24
+
25
+ if (value.length === 0) {
26
+ regex.lastIndex += 1;
27
+ }
28
+ }
29
+ }
30
+
31
+ return hits;
32
+ }
33
+
34
+ function collapseOverlaps(detections: SecretDetection[]): SecretDetection[] {
35
+ const sorted = [...detections].sort((a, b) => {
36
+ if (a.start !== b.start) {
37
+ return a.start - b.start;
38
+ }
39
+ return b.end - a.end;
40
+ });
41
+
42
+ const accepted: SecretDetection[] = [];
43
+ for (const detection of sorted) {
44
+ const previous = accepted[accepted.length - 1];
45
+ if (!previous || detection.start >= previous.end) {
46
+ accepted.push(detection);
47
+ }
48
+ }
49
+
50
+ return accepted;
51
+ }
52
+
53
+ export function scanText(text: string): SecretDetection[] {
54
+ return collapseOverlaps(runPatternScan(text));
55
+ }
56
+
57
+ export function redactText(text: string): string {
58
+ const detections = scanText(text);
59
+ if (detections.length === 0) {
60
+ return text;
61
+ }
62
+
63
+ let result = "";
64
+ let cursor = 0;
65
+
66
+ for (const detection of detections) {
67
+ result += text.slice(cursor, detection.start);
68
+ result += `[REDACTED:${detection.type}]`;
69
+ cursor = detection.end;
70
+ }
71
+
72
+ result += text.slice(cursor);
73
+ return result;
74
+ }
75
+
76
+ export function redactFile(filePath: string): string {
77
+ const content = readFileSync(filePath, "utf8");
78
+ return redactText(content);
79
+ }
@@ -0,0 +1,33 @@
1
+ import { strict as assert } from "node:assert";
2
+ import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { redactFile, redactText, scanText } from "../src/redact";
5
+
6
+ function run(): void {
7
+ const fixturePath = join(__dirname, "..", "test", "sample-secrets.txt");
8
+ const fixture = readFileSync(fixturePath, "utf8");
9
+
10
+ const detections = scanText(fixture);
11
+ assert.ok(detections.length >= 10, "should detect multiple secrets");
12
+
13
+ const redacted = redactText(fixture);
14
+ assert.ok(redacted.includes("[REDACTED:openai_project_key]"));
15
+ assert.ok(redacted.includes("[REDACTED:anthropic_api_key]"));
16
+ assert.ok(redacted.includes("[REDACTED:github_token_ghp]"));
17
+ assert.ok(redacted.includes("[REDACTED:aws_access_key]"));
18
+ assert.ok(redacted.includes("[REDACTED:sendgrid_api_key]"));
19
+
20
+ const fromFile = redactFile(fixturePath);
21
+ assert.equal(fromFile, redacted, "redactFile should match redactText output");
22
+
23
+ const codeFixturePath = join(__dirname, "..", "test", "sample-code.ts");
24
+ const codeFixture = readFileSync(codeFixturePath, "utf8");
25
+ const codeRedacted = redactText(codeFixture);
26
+ assert.ok(codeRedacted.includes("[REDACTED:openai_project_key]"));
27
+ assert.ok(codeRedacted.includes("[REDACTED:github_token_ghp]"));
28
+ assert.ok(codeRedacted.includes("[REDACTED:postgres_url]"));
29
+
30
+ console.log("All redact tests passed.");
31
+ }
32
+
33
+ run();
@@ -0,0 +1,4 @@
1
+ const openaiKey = 'sk-proj-xK9mR2vNqL4pW8yT3uC7aE1sD6fH5jB0nZwQ';
2
+ const githubToken = 'ghp_xK9mR2vNqL4pW8yT3uC7aE1sD6fH5jB0nZ';
3
+ headers: { 'Authorization': 'Bearer sk-proj-xK9mR2vNqL4pW8yT3uC7aE1sD6fH5jB0nZwQ' }
4
+ const db = new Database('postgres://admin:SuperSecret123!@db.example.com:5432/proddb');
@@ -0,0 +1,10 @@
1
+ OPENAI_API_KEY=sk-proj-xK9mR2vNqL4pW8yT3uC7aE1sD6fH5jB0nZwQ
2
+ ANTHROPIC_API_KEY=sk-ant-api03-xK9mR2vNqL4pW8yT3uC7aE1sD6fH5jB0nZwQabcdef1234567890abcdef12
3
+ GITHUB_TOKEN=ghp_xK9mR2vNqL4pW8yT3uC7aE1sD6fH5jB0nZ
4
+ AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
5
+ AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
6
+ STRIPE_SECRET_KEY=sk_live_xK9mR2vNqL4pW8yT3uC7aE1sD6fH5jB0nZ
7
+ DATABASE_URL=postgres://admin:SuperSecret123!@db.example.com:5432/proddb
8
+ REDIS_URL=redis://:myredispassword@redis.example.com:6379
9
+ SLACK_BOT_TOKEN=xoxb-1234567890-1234567890123-xK9mR2vNqL4pW8yT3uC7
10
+ SENDGRID_API_KEY=SG.xK9mR2vNqL4pW8yT3uC7aE1sD6fH5jB0nZwQabcdef12
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "CommonJS",
5
+ "moduleResolution": "Node",
6
+ "outDir": "dist",
7
+ "rootDir": ".",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "skipLibCheck": true,
12
+ "declaration": true,
13
+ "sourceMap": true
14
+ },
15
+ "include": ["src/**/*.ts", "test/redact.test.ts"]
16
+ }