@blursec/mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Blursec
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 ADDED
@@ -0,0 +1,99 @@
1
+ # @blursec/mcp
2
+
3
+ Official [Model Context Protocol](https://modelcontextprotocol.io) server for
4
+ [`@blursec/sdk`](https://www.npmjs.com/package/@blursec/sdk) — lets AI agents
5
+ (Claude, GitHub Copilot, Cursor, custom agents) check credentials, session
6
+ tokens, and hashes against live stealer-log threat intel.
7
+
8
+ > TL;DR — one `npx` command turns Blursec into five agent tools. All hashing
9
+ > happens locally; only a 10-character k-anonymity prefix ever leaves the
10
+ > machine. Zero runtime dependencies — the protocol core is hand-rolled.
11
+
12
+ ## Install & run
13
+
14
+ ```bash
15
+ npm i -g @blursec/mcp @blursec/sdk # or run via npx, see below
16
+ ```
17
+
18
+ The server reads its configuration from the environment:
19
+
20
+ | env var | purpose |
21
+ |-----------------------|----------------------------------------------|
22
+ | `BLURSEC_API_KEY` | API key (required) |
23
+ | `BLURSEC_ENVIRONMENT` | `production` (default) / `staging` / `local` |
24
+
25
+ Never pass the API key as prompt text or argv — use the host's `env` block.
26
+
27
+ ### Claude Desktop / Claude Code
28
+
29
+ ```json
30
+ {
31
+ "mcpServers": {
32
+ "blursec": {
33
+ "command": "npx",
34
+ "args": ["-y", "@blursec/mcp"],
35
+ "env": { "BLURSEC_API_KEY": "bsk_live_..." }
36
+ }
37
+ }
38
+ }
39
+ ```
40
+
41
+ ### VS Code (GitHub Copilot)
42
+
43
+ `.vscode/mcp.json`:
44
+
45
+ ```json
46
+ {
47
+ "servers": {
48
+ "blursec": {
49
+ "type": "stdio",
50
+ "command": "npx",
51
+ "args": ["-y", "@blursec/mcp"],
52
+ "env": { "BLURSEC_API_KEY": "${input:blursec-api-key}" }
53
+ }
54
+ }
55
+ }
56
+ ```
57
+
58
+ ## Tools
59
+
60
+ | Tool | Wraps | Use it for |
61
+ |------|-------|------------|
62
+ | `blursec_check_credential` | `credentials.check` | "Is this email+password leaked?" |
63
+ | `blursec_check_token` | `credentials.checkToken` | Session cookie / bearer token triage |
64
+ | `blursec_check_hash` | `credentials.checkHash` | Pre-hashed checks — **most private**, the raw secret never enters the conversation |
65
+ | `blursec_verify_session` | `sessions.verify` | Incident response on live sessions (auto-detects JWT vs opaque) |
66
+ | `blursec_whoami` | `auth.whoami` | Verify connectivity + key scope |
67
+
68
+ Deliberately **not** exposed: `auth.rotate` — it invalidates the live API key
69
+ immediately, which is far too destructive to hand to an autonomous agent.
70
+
71
+ ## Security model
72
+
73
+ - **K-anonymity preserved** — hashing happens inside the server process via
74
+ the SDK; only the 10-hex-char prefix is transmitted to the Blursec API.
75
+ - **No secret echo** — tool results never repeat the input credential/token,
76
+ so secrets are not duplicated into the model context.
77
+ - **Fail-open is explicit** — results carry `failedOpen: true` when the
78
+ database was unreachable. The server's `initialize` instructions tell the
79
+ model to treat that as *unknown*, never as *safe*.
80
+ - **Zero runtime dependencies** — like the SDK itself, this package ships no
81
+ third-party code. The MCP protocol subset (stdio framing, initialize,
82
+ tools) is implemented in ~200 audited lines.
83
+
84
+ ## Embedding in-process
85
+
86
+ The protocol core is exported for hosts that want a custom transport:
87
+
88
+ ```ts
89
+ import { BlursecMcpServer, createBlursecFromEnv } from "@blursec/mcp";
90
+
91
+ const server = new BlursecMcpServer(createBlursecFromEnv());
92
+
93
+ // Feed it newline-delimited JSON-RPC; undefined means "notification, no reply".
94
+ const reply = await server.handleLine(line);
95
+ ```
96
+
97
+ ## License
98
+
99
+ MIT © Blursec
package/dist/cli.cjs ADDED
@@ -0,0 +1,406 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ var readline = require('readline');
5
+ var sdk = require('@blursec/sdk');
6
+
7
+ var PLATFORM_HEADER = "X-Blursec-Platform";
8
+ function resolveEnvironment(value) {
9
+ switch (value) {
10
+ case "production":
11
+ case "staging":
12
+ case "local":
13
+ return value;
14
+ default:
15
+ return "production";
16
+ }
17
+ }
18
+ function readEnvString(name) {
19
+ const value = process.env[name];
20
+ if (typeof value !== "string") return void 0;
21
+ const trimmed = value.trim();
22
+ return trimmed.length > 0 ? trimmed : void 0;
23
+ }
24
+ function createBlursecFromEnv(overrides) {
25
+ const apiKey = readEnvString("BLURSEC_API_KEY");
26
+ if (typeof apiKey !== "string" || apiKey.trim().length === 0) {
27
+ throw new sdk.BlursecValidationError(
28
+ "blursec-mcp: BLURSEC_API_KEY is not set. Add it to the `env` block of your MCP host configuration (never pass API keys as prompt text)."
29
+ );
30
+ }
31
+ const environment = resolveEnvironment(readEnvString("BLURSEC_ENVIRONMENT"));
32
+ const mergedHeaders = {
33
+ [PLATFORM_HEADER]: "mcp",
34
+ ...{}
35
+ };
36
+ return new sdk.Blursec({
37
+ ...overrides,
38
+ apiKey,
39
+ environment,
40
+ defaultHeaders: mergedHeaders
41
+ });
42
+ }
43
+ var UnknownToolError = class extends Error {
44
+ constructor(name) {
45
+ super(`Unknown tool: ${name}`);
46
+ this.name = "UnknownToolError";
47
+ Object.setPrototypeOf(this, new.target.prototype);
48
+ }
49
+ };
50
+ var CONTEXT_SCHEMA = {
51
+ type: "object",
52
+ description: "Optional request context forwarded to Blursec Layer-3 risk scoring.",
53
+ properties: {
54
+ ip: { type: "string", description: "Client IP address." },
55
+ userAgent: { type: "string", description: "Raw User-Agent header." },
56
+ deviceFingerprint: { type: "string", description: "Stable device fingerprint." },
57
+ country: { type: "string", description: "ISO 3166-1 alpha-2 country code." },
58
+ userId: { type: "string", description: "User id, if known at check time." }
59
+ },
60
+ additionalProperties: false
61
+ };
62
+ var BLURSEC_TOOLS = [
63
+ {
64
+ name: "blursec_check_credential",
65
+ description: "Check an email + password pair against the live Blursec stealer-log database using k-anonymity (only a 10-character SHA-256 prefix leaves this machine; the raw password is hashed locally and never transmitted or echoed back). Returns leaked status, severity, and a recommended action (allow / monitor / force_reset / kill_sessions / block). If you already have the SHA-256 digest, prefer blursec_check_hash so the raw secret never enters the conversation.",
66
+ inputSchema: {
67
+ type: "object",
68
+ properties: {
69
+ email: { type: "string", description: "The account email address." },
70
+ password: { type: "string", description: "The password to check." },
71
+ context: CONTEXT_SCHEMA
72
+ },
73
+ required: ["email", "password"],
74
+ additionalProperties: false
75
+ }
76
+ },
77
+ {
78
+ name: "blursec_check_token",
79
+ description: "Check a session token / cookie value against the Blursec stealer-log database. Same k-anonymity model as blursec_check_credential \u2014 the token is hashed locally and only a 10-character prefix is transmitted.",
80
+ inputSchema: {
81
+ type: "object",
82
+ properties: {
83
+ sessionToken: {
84
+ type: "string",
85
+ description: "The raw session token or cookie value."
86
+ },
87
+ context: CONTEXT_SCHEMA
88
+ },
89
+ required: ["sessionToken"],
90
+ additionalProperties: false
91
+ }
92
+ },
93
+ {
94
+ name: "blursec_check_hash",
95
+ description: "Check a pre-computed SHA-256 hex digest (64 characters) against the Blursec stealer-log database. The most privacy-preserving tool: the raw credential never needs to appear anywhere in the conversation. For email+password pairs the digest must be SHA-256(lowercase(trim(email)) + ':' + password).",
96
+ inputSchema: {
97
+ type: "object",
98
+ properties: {
99
+ sha256: {
100
+ type: "string",
101
+ description: "64-character SHA-256 hex digest of the credential.",
102
+ pattern: "^[0-9a-fA-F]{64}$"
103
+ },
104
+ context: CONTEXT_SCHEMA
105
+ },
106
+ required: ["sha256"],
107
+ additionalProperties: false
108
+ }
109
+ },
110
+ {
111
+ name: "blursec_verify_session",
112
+ description: "Verify a live session token against the stealer-log database. Like blursec_check_token but auto-detects the token shape (JWTs are hashed by signature segment only) and additionally returns tokenType and a compromised flag. Use during incident response to triage active sessions.",
113
+ inputSchema: {
114
+ type: "object",
115
+ properties: {
116
+ sessionToken: {
117
+ type: "string",
118
+ description: "The raw session token (opaque or JWT)."
119
+ }
120
+ },
121
+ required: ["sessionToken"],
122
+ additionalProperties: false
123
+ }
124
+ },
125
+ {
126
+ name: "blursec_whoami",
127
+ description: "Return the principal the configured Blursec API key is authenticated as (id, name, workspace, scopes). Use to verify connectivity and key scope before running checks.",
128
+ inputSchema: {
129
+ type: "object",
130
+ properties: {},
131
+ additionalProperties: false
132
+ }
133
+ }
134
+ ];
135
+ async function callBlursecTool(client, name, args) {
136
+ try {
137
+ switch (name) {
138
+ case "blursec_check_credential": {
139
+ const bag = requireArgsObject(args);
140
+ const email = requireString(bag, "email");
141
+ const password = requireString(bag, "password");
142
+ const result = await client.credentials.check(
143
+ email,
144
+ password,
145
+ checkOptions(bag)
146
+ );
147
+ return { ok: true, payload: result };
148
+ }
149
+ case "blursec_check_token": {
150
+ const bag = requireArgsObject(args);
151
+ const sessionToken = requireString(bag, "sessionToken");
152
+ const result = await client.credentials.checkToken(
153
+ sessionToken,
154
+ checkOptions(bag)
155
+ );
156
+ return { ok: true, payload: result };
157
+ }
158
+ case "blursec_check_hash": {
159
+ const bag = requireArgsObject(args);
160
+ const sha256 = requireString(bag, "sha256");
161
+ const result = await client.credentials.checkHash(
162
+ sha256,
163
+ checkOptions(bag)
164
+ );
165
+ return { ok: true, payload: result };
166
+ }
167
+ case "blursec_verify_session": {
168
+ const bag = requireArgsObject(args);
169
+ const sessionToken = requireString(bag, "sessionToken");
170
+ const result = await client.sessions.verify(sessionToken);
171
+ return { ok: true, payload: result };
172
+ }
173
+ case "blursec_whoami": {
174
+ const result = await client.auth.whoami();
175
+ return { ok: true, payload: result };
176
+ }
177
+ default:
178
+ throw new UnknownToolError(name);
179
+ }
180
+ } catch (err) {
181
+ if (err instanceof UnknownToolError) throw err;
182
+ return {
183
+ ok: false,
184
+ payload: {
185
+ error: err instanceof Error ? err.message : String(err),
186
+ type: err instanceof Error ? err.name : "UnknownError"
187
+ }
188
+ };
189
+ }
190
+ }
191
+ function requireArgsObject(args) {
192
+ if (typeof args !== "object" || args === null || Array.isArray(args)) {
193
+ throw new sdk.BlursecValidationError(
194
+ "tool arguments must be a JSON object."
195
+ );
196
+ }
197
+ return args;
198
+ }
199
+ function requireString(bag, key) {
200
+ const value = bag[key];
201
+ if (typeof value !== "string" || value.length === 0) {
202
+ throw new sdk.BlursecValidationError(
203
+ `missing or empty required string argument: ${key}`
204
+ );
205
+ }
206
+ return value;
207
+ }
208
+ function checkOptions(bag) {
209
+ const raw = bag["context"];
210
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
211
+ return void 0;
212
+ }
213
+ const source = raw;
214
+ const context = {};
215
+ for (const key of [
216
+ "ip",
217
+ "userAgent",
218
+ "deviceFingerprint",
219
+ "country",
220
+ "userId"
221
+ ]) {
222
+ const value = source[key];
223
+ if (typeof value === "string" && value.length > 0) {
224
+ context[key] = value;
225
+ }
226
+ }
227
+ return Object.keys(context).length > 0 ? { context } : void 0;
228
+ }
229
+
230
+ // src/server.ts
231
+ var SUPPORTED_PROTOCOL_VERSIONS = [
232
+ "2025-06-18",
233
+ "2025-03-26",
234
+ "2024-11-05"
235
+ ];
236
+ var JSONRPC_ERRORS = {
237
+ PARSE_ERROR: -32700,
238
+ INVALID_REQUEST: -32600,
239
+ METHOD_NOT_FOUND: -32601,
240
+ INVALID_PARAMS: -32602
241
+ };
242
+ var DEFAULT_SERVER_INFO = {
243
+ name: "blursec-mcp",
244
+ version: "1.0.0"
245
+ };
246
+ var BlursecMcpServer = class {
247
+ client;
248
+ info;
249
+ constructor(client, info = DEFAULT_SERVER_INFO) {
250
+ this.client = client;
251
+ this.info = info;
252
+ }
253
+ /**
254
+ * Handle one newline-delimited JSON-RPC message and return the
255
+ * serialized reply, or `undefined` for notifications (which per
256
+ * JSON-RPC must not be answered).
257
+ */
258
+ async handleLine(line) {
259
+ let message;
260
+ try {
261
+ message = JSON.parse(line);
262
+ } catch {
263
+ return JSON.stringify({
264
+ jsonrpc: "2.0",
265
+ id: null,
266
+ error: { code: JSONRPC_ERRORS.PARSE_ERROR, message: "Parse error" }
267
+ });
268
+ }
269
+ const response = await this.handleMessage(message);
270
+ return response === void 0 ? void 0 : JSON.stringify(response);
271
+ }
272
+ /**
273
+ * Handle one already-parsed JSON-RPC message.
274
+ *
275
+ * Returns `undefined` for notifications; a {@link JsonRpcResponse}
276
+ * for requests (including error responses — this method never throws).
277
+ */
278
+ async handleMessage(message) {
279
+ if (!isRequestShaped(message)) {
280
+ return {
281
+ jsonrpc: "2.0",
282
+ id: null,
283
+ error: {
284
+ code: JSONRPC_ERRORS.INVALID_REQUEST,
285
+ message: "Invalid Request"
286
+ }
287
+ };
288
+ }
289
+ const { id, method, params } = message;
290
+ const isNotification = id === void 0;
291
+ if (isNotification) return void 0;
292
+ switch (method) {
293
+ case "initialize":
294
+ return this.ok(id, this.initializeResult(params));
295
+ case "ping":
296
+ return this.ok(id, {});
297
+ case "tools/list":
298
+ return this.ok(id, { tools: BLURSEC_TOOLS });
299
+ case "tools/call":
300
+ return this.toolsCall(id, params);
301
+ default:
302
+ return this.err(
303
+ id,
304
+ JSONRPC_ERRORS.METHOD_NOT_FOUND,
305
+ `Method not found: ${method}`
306
+ );
307
+ }
308
+ }
309
+ // ── handlers ────────────────────────────────────────────────────────
310
+ initializeResult(params) {
311
+ const requested = typeof params === "object" && params !== null && typeof params["protocolVersion"] === "string" ? params["protocolVersion"] : void 0;
312
+ const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.includes(requested ?? "") ? requested : SUPPORTED_PROTOCOL_VERSIONS[0];
313
+ return {
314
+ protocolVersion,
315
+ capabilities: { tools: {} },
316
+ serverInfo: this.info,
317
+ instructions: "Blursec credential-leak threat intel. All check tools hash locally and transmit only a 10-character k-anonymity prefix. Prefer blursec_check_hash when a SHA-256 digest is available so raw secrets stay out of the conversation. Results with failedOpen=true mean the database was unreachable \u2014 treat as 'unknown', never as 'safe'."
318
+ };
319
+ }
320
+ async toolsCall(id, params) {
321
+ if (typeof params !== "object" || params === null || typeof params["name"] !== "string") {
322
+ return this.err(
323
+ id,
324
+ JSONRPC_ERRORS.INVALID_PARAMS,
325
+ "tools/call requires params.name (string)"
326
+ );
327
+ }
328
+ const name = params["name"];
329
+ const args = params["arguments"] ?? {};
330
+ try {
331
+ const outcome = await callBlursecTool(this.client, name, args);
332
+ return this.ok(id, {
333
+ content: [
334
+ {
335
+ type: "text",
336
+ text: JSON.stringify(outcome.payload, null, 2)
337
+ }
338
+ ],
339
+ isError: !outcome.ok
340
+ });
341
+ } catch (err) {
342
+ if (err instanceof UnknownToolError) {
343
+ return this.err(id, JSONRPC_ERRORS.INVALID_PARAMS, err.message);
344
+ }
345
+ return this.err(
346
+ id,
347
+ JSONRPC_ERRORS.INVALID_REQUEST,
348
+ err instanceof Error ? err.message : String(err)
349
+ );
350
+ }
351
+ }
352
+ // ── reply builders ──────────────────────────────────────────────────
353
+ ok(id, result) {
354
+ return { jsonrpc: "2.0", id, result };
355
+ }
356
+ err(id, code, message) {
357
+ return { jsonrpc: "2.0", id, error: { code, message } };
358
+ }
359
+ };
360
+ function isRequestShaped(message) {
361
+ if (typeof message !== "object" || message === null) return false;
362
+ const m = message;
363
+ if (m["jsonrpc"] !== "2.0") return false;
364
+ if (typeof m["method"] !== "string") return false;
365
+ const id = m["id"];
366
+ return id === void 0 || id === null || typeof id === "string" || typeof id === "number";
367
+ }
368
+
369
+ // src/cli.ts
370
+ function main() {
371
+ let server;
372
+ try {
373
+ server = new BlursecMcpServer(createBlursecFromEnv());
374
+ } catch (err) {
375
+ process.stderr.write(
376
+ `blursec-mcp: ${err instanceof Error ? err.message : String(err)}
377
+ `
378
+ );
379
+ process.exitCode = 1;
380
+ return;
381
+ }
382
+ const rl = readline.createInterface({ input: process.stdin, terminal: false });
383
+ let pipeline = Promise.resolve();
384
+ rl.on("line", (line) => {
385
+ if (line.trim().length === 0) return;
386
+ pipeline = pipeline.then(async () => {
387
+ try {
388
+ const reply = await server.handleLine(line);
389
+ if (reply !== void 0) process.stdout.write(reply + "\n");
390
+ } catch (err) {
391
+ process.stderr.write(
392
+ `blursec-mcp: internal error: ${err instanceof Error ? err.message : String(err)}
393
+ `
394
+ );
395
+ }
396
+ });
397
+ });
398
+ rl.on("close", () => {
399
+ void pipeline.then(() => {
400
+ process.exitCode = 0;
401
+ });
402
+ });
403
+ }
404
+ main();
405
+ //# sourceMappingURL=cli.cjs.map
406
+ //# sourceMappingURL=cli.cjs.map
package/dist/cli.d.cts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node