@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 +21 -0
- package/README.md +99 -0
- package/dist/cli.cjs +406 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +404 -0
- package/dist/index.cjs +376 -0
- package/dist/index.d.cts +173 -0
- package/dist/index.d.ts +173 -0
- package/dist/index.js +368 -0
- package/package.json +75 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createInterface } from 'readline';
|
|
3
|
+
import { BlursecValidationError, Blursec } from '@blursec/sdk';
|
|
4
|
+
|
|
5
|
+
var PLATFORM_HEADER = "X-Blursec-Platform";
|
|
6
|
+
function resolveEnvironment(value) {
|
|
7
|
+
switch (value) {
|
|
8
|
+
case "production":
|
|
9
|
+
case "staging":
|
|
10
|
+
case "local":
|
|
11
|
+
return value;
|
|
12
|
+
default:
|
|
13
|
+
return "production";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function readEnvString(name) {
|
|
17
|
+
const value = process.env[name];
|
|
18
|
+
if (typeof value !== "string") return void 0;
|
|
19
|
+
const trimmed = value.trim();
|
|
20
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
21
|
+
}
|
|
22
|
+
function createBlursecFromEnv(overrides) {
|
|
23
|
+
const apiKey = readEnvString("BLURSEC_API_KEY");
|
|
24
|
+
if (typeof apiKey !== "string" || apiKey.trim().length === 0) {
|
|
25
|
+
throw new BlursecValidationError(
|
|
26
|
+
"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)."
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
const environment = resolveEnvironment(readEnvString("BLURSEC_ENVIRONMENT"));
|
|
30
|
+
const mergedHeaders = {
|
|
31
|
+
[PLATFORM_HEADER]: "mcp",
|
|
32
|
+
...{}
|
|
33
|
+
};
|
|
34
|
+
return new Blursec({
|
|
35
|
+
...overrides,
|
|
36
|
+
apiKey,
|
|
37
|
+
environment,
|
|
38
|
+
defaultHeaders: mergedHeaders
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
var UnknownToolError = class extends Error {
|
|
42
|
+
constructor(name) {
|
|
43
|
+
super(`Unknown tool: ${name}`);
|
|
44
|
+
this.name = "UnknownToolError";
|
|
45
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
var CONTEXT_SCHEMA = {
|
|
49
|
+
type: "object",
|
|
50
|
+
description: "Optional request context forwarded to Blursec Layer-3 risk scoring.",
|
|
51
|
+
properties: {
|
|
52
|
+
ip: { type: "string", description: "Client IP address." },
|
|
53
|
+
userAgent: { type: "string", description: "Raw User-Agent header." },
|
|
54
|
+
deviceFingerprint: { type: "string", description: "Stable device fingerprint." },
|
|
55
|
+
country: { type: "string", description: "ISO 3166-1 alpha-2 country code." },
|
|
56
|
+
userId: { type: "string", description: "User id, if known at check time." }
|
|
57
|
+
},
|
|
58
|
+
additionalProperties: false
|
|
59
|
+
};
|
|
60
|
+
var BLURSEC_TOOLS = [
|
|
61
|
+
{
|
|
62
|
+
name: "blursec_check_credential",
|
|
63
|
+
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.",
|
|
64
|
+
inputSchema: {
|
|
65
|
+
type: "object",
|
|
66
|
+
properties: {
|
|
67
|
+
email: { type: "string", description: "The account email address." },
|
|
68
|
+
password: { type: "string", description: "The password to check." },
|
|
69
|
+
context: CONTEXT_SCHEMA
|
|
70
|
+
},
|
|
71
|
+
required: ["email", "password"],
|
|
72
|
+
additionalProperties: false
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: "blursec_check_token",
|
|
77
|
+
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.",
|
|
78
|
+
inputSchema: {
|
|
79
|
+
type: "object",
|
|
80
|
+
properties: {
|
|
81
|
+
sessionToken: {
|
|
82
|
+
type: "string",
|
|
83
|
+
description: "The raw session token or cookie value."
|
|
84
|
+
},
|
|
85
|
+
context: CONTEXT_SCHEMA
|
|
86
|
+
},
|
|
87
|
+
required: ["sessionToken"],
|
|
88
|
+
additionalProperties: false
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: "blursec_check_hash",
|
|
93
|
+
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).",
|
|
94
|
+
inputSchema: {
|
|
95
|
+
type: "object",
|
|
96
|
+
properties: {
|
|
97
|
+
sha256: {
|
|
98
|
+
type: "string",
|
|
99
|
+
description: "64-character SHA-256 hex digest of the credential.",
|
|
100
|
+
pattern: "^[0-9a-fA-F]{64}$"
|
|
101
|
+
},
|
|
102
|
+
context: CONTEXT_SCHEMA
|
|
103
|
+
},
|
|
104
|
+
required: ["sha256"],
|
|
105
|
+
additionalProperties: false
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: "blursec_verify_session",
|
|
110
|
+
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.",
|
|
111
|
+
inputSchema: {
|
|
112
|
+
type: "object",
|
|
113
|
+
properties: {
|
|
114
|
+
sessionToken: {
|
|
115
|
+
type: "string",
|
|
116
|
+
description: "The raw session token (opaque or JWT)."
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
required: ["sessionToken"],
|
|
120
|
+
additionalProperties: false
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: "blursec_whoami",
|
|
125
|
+
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.",
|
|
126
|
+
inputSchema: {
|
|
127
|
+
type: "object",
|
|
128
|
+
properties: {},
|
|
129
|
+
additionalProperties: false
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
];
|
|
133
|
+
async function callBlursecTool(client, name, args) {
|
|
134
|
+
try {
|
|
135
|
+
switch (name) {
|
|
136
|
+
case "blursec_check_credential": {
|
|
137
|
+
const bag = requireArgsObject(args);
|
|
138
|
+
const email = requireString(bag, "email");
|
|
139
|
+
const password = requireString(bag, "password");
|
|
140
|
+
const result = await client.credentials.check(
|
|
141
|
+
email,
|
|
142
|
+
password,
|
|
143
|
+
checkOptions(bag)
|
|
144
|
+
);
|
|
145
|
+
return { ok: true, payload: result };
|
|
146
|
+
}
|
|
147
|
+
case "blursec_check_token": {
|
|
148
|
+
const bag = requireArgsObject(args);
|
|
149
|
+
const sessionToken = requireString(bag, "sessionToken");
|
|
150
|
+
const result = await client.credentials.checkToken(
|
|
151
|
+
sessionToken,
|
|
152
|
+
checkOptions(bag)
|
|
153
|
+
);
|
|
154
|
+
return { ok: true, payload: result };
|
|
155
|
+
}
|
|
156
|
+
case "blursec_check_hash": {
|
|
157
|
+
const bag = requireArgsObject(args);
|
|
158
|
+
const sha256 = requireString(bag, "sha256");
|
|
159
|
+
const result = await client.credentials.checkHash(
|
|
160
|
+
sha256,
|
|
161
|
+
checkOptions(bag)
|
|
162
|
+
);
|
|
163
|
+
return { ok: true, payload: result };
|
|
164
|
+
}
|
|
165
|
+
case "blursec_verify_session": {
|
|
166
|
+
const bag = requireArgsObject(args);
|
|
167
|
+
const sessionToken = requireString(bag, "sessionToken");
|
|
168
|
+
const result = await client.sessions.verify(sessionToken);
|
|
169
|
+
return { ok: true, payload: result };
|
|
170
|
+
}
|
|
171
|
+
case "blursec_whoami": {
|
|
172
|
+
const result = await client.auth.whoami();
|
|
173
|
+
return { ok: true, payload: result };
|
|
174
|
+
}
|
|
175
|
+
default:
|
|
176
|
+
throw new UnknownToolError(name);
|
|
177
|
+
}
|
|
178
|
+
} catch (err) {
|
|
179
|
+
if (err instanceof UnknownToolError) throw err;
|
|
180
|
+
return {
|
|
181
|
+
ok: false,
|
|
182
|
+
payload: {
|
|
183
|
+
error: err instanceof Error ? err.message : String(err),
|
|
184
|
+
type: err instanceof Error ? err.name : "UnknownError"
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function requireArgsObject(args) {
|
|
190
|
+
if (typeof args !== "object" || args === null || Array.isArray(args)) {
|
|
191
|
+
throw new BlursecValidationError(
|
|
192
|
+
"tool arguments must be a JSON object."
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
return args;
|
|
196
|
+
}
|
|
197
|
+
function requireString(bag, key) {
|
|
198
|
+
const value = bag[key];
|
|
199
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
200
|
+
throw new BlursecValidationError(
|
|
201
|
+
`missing or empty required string argument: ${key}`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
return value;
|
|
205
|
+
}
|
|
206
|
+
function checkOptions(bag) {
|
|
207
|
+
const raw = bag["context"];
|
|
208
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
209
|
+
return void 0;
|
|
210
|
+
}
|
|
211
|
+
const source = raw;
|
|
212
|
+
const context = {};
|
|
213
|
+
for (const key of [
|
|
214
|
+
"ip",
|
|
215
|
+
"userAgent",
|
|
216
|
+
"deviceFingerprint",
|
|
217
|
+
"country",
|
|
218
|
+
"userId"
|
|
219
|
+
]) {
|
|
220
|
+
const value = source[key];
|
|
221
|
+
if (typeof value === "string" && value.length > 0) {
|
|
222
|
+
context[key] = value;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return Object.keys(context).length > 0 ? { context } : void 0;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/server.ts
|
|
229
|
+
var SUPPORTED_PROTOCOL_VERSIONS = [
|
|
230
|
+
"2025-06-18",
|
|
231
|
+
"2025-03-26",
|
|
232
|
+
"2024-11-05"
|
|
233
|
+
];
|
|
234
|
+
var JSONRPC_ERRORS = {
|
|
235
|
+
PARSE_ERROR: -32700,
|
|
236
|
+
INVALID_REQUEST: -32600,
|
|
237
|
+
METHOD_NOT_FOUND: -32601,
|
|
238
|
+
INVALID_PARAMS: -32602
|
|
239
|
+
};
|
|
240
|
+
var DEFAULT_SERVER_INFO = {
|
|
241
|
+
name: "blursec-mcp",
|
|
242
|
+
version: "1.0.0"
|
|
243
|
+
};
|
|
244
|
+
var BlursecMcpServer = class {
|
|
245
|
+
client;
|
|
246
|
+
info;
|
|
247
|
+
constructor(client, info = DEFAULT_SERVER_INFO) {
|
|
248
|
+
this.client = client;
|
|
249
|
+
this.info = info;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Handle one newline-delimited JSON-RPC message and return the
|
|
253
|
+
* serialized reply, or `undefined` for notifications (which per
|
|
254
|
+
* JSON-RPC must not be answered).
|
|
255
|
+
*/
|
|
256
|
+
async handleLine(line) {
|
|
257
|
+
let message;
|
|
258
|
+
try {
|
|
259
|
+
message = JSON.parse(line);
|
|
260
|
+
} catch {
|
|
261
|
+
return JSON.stringify({
|
|
262
|
+
jsonrpc: "2.0",
|
|
263
|
+
id: null,
|
|
264
|
+
error: { code: JSONRPC_ERRORS.PARSE_ERROR, message: "Parse error" }
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
const response = await this.handleMessage(message);
|
|
268
|
+
return response === void 0 ? void 0 : JSON.stringify(response);
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Handle one already-parsed JSON-RPC message.
|
|
272
|
+
*
|
|
273
|
+
* Returns `undefined` for notifications; a {@link JsonRpcResponse}
|
|
274
|
+
* for requests (including error responses — this method never throws).
|
|
275
|
+
*/
|
|
276
|
+
async handleMessage(message) {
|
|
277
|
+
if (!isRequestShaped(message)) {
|
|
278
|
+
return {
|
|
279
|
+
jsonrpc: "2.0",
|
|
280
|
+
id: null,
|
|
281
|
+
error: {
|
|
282
|
+
code: JSONRPC_ERRORS.INVALID_REQUEST,
|
|
283
|
+
message: "Invalid Request"
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
const { id, method, params } = message;
|
|
288
|
+
const isNotification = id === void 0;
|
|
289
|
+
if (isNotification) return void 0;
|
|
290
|
+
switch (method) {
|
|
291
|
+
case "initialize":
|
|
292
|
+
return this.ok(id, this.initializeResult(params));
|
|
293
|
+
case "ping":
|
|
294
|
+
return this.ok(id, {});
|
|
295
|
+
case "tools/list":
|
|
296
|
+
return this.ok(id, { tools: BLURSEC_TOOLS });
|
|
297
|
+
case "tools/call":
|
|
298
|
+
return this.toolsCall(id, params);
|
|
299
|
+
default:
|
|
300
|
+
return this.err(
|
|
301
|
+
id,
|
|
302
|
+
JSONRPC_ERRORS.METHOD_NOT_FOUND,
|
|
303
|
+
`Method not found: ${method}`
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
// ── handlers ────────────────────────────────────────────────────────
|
|
308
|
+
initializeResult(params) {
|
|
309
|
+
const requested = typeof params === "object" && params !== null && typeof params["protocolVersion"] === "string" ? params["protocolVersion"] : void 0;
|
|
310
|
+
const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.includes(requested ?? "") ? requested : SUPPORTED_PROTOCOL_VERSIONS[0];
|
|
311
|
+
return {
|
|
312
|
+
protocolVersion,
|
|
313
|
+
capabilities: { tools: {} },
|
|
314
|
+
serverInfo: this.info,
|
|
315
|
+
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'."
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
async toolsCall(id, params) {
|
|
319
|
+
if (typeof params !== "object" || params === null || typeof params["name"] !== "string") {
|
|
320
|
+
return this.err(
|
|
321
|
+
id,
|
|
322
|
+
JSONRPC_ERRORS.INVALID_PARAMS,
|
|
323
|
+
"tools/call requires params.name (string)"
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
const name = params["name"];
|
|
327
|
+
const args = params["arguments"] ?? {};
|
|
328
|
+
try {
|
|
329
|
+
const outcome = await callBlursecTool(this.client, name, args);
|
|
330
|
+
return this.ok(id, {
|
|
331
|
+
content: [
|
|
332
|
+
{
|
|
333
|
+
type: "text",
|
|
334
|
+
text: JSON.stringify(outcome.payload, null, 2)
|
|
335
|
+
}
|
|
336
|
+
],
|
|
337
|
+
isError: !outcome.ok
|
|
338
|
+
});
|
|
339
|
+
} catch (err) {
|
|
340
|
+
if (err instanceof UnknownToolError) {
|
|
341
|
+
return this.err(id, JSONRPC_ERRORS.INVALID_PARAMS, err.message);
|
|
342
|
+
}
|
|
343
|
+
return this.err(
|
|
344
|
+
id,
|
|
345
|
+
JSONRPC_ERRORS.INVALID_REQUEST,
|
|
346
|
+
err instanceof Error ? err.message : String(err)
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// ── reply builders ──────────────────────────────────────────────────
|
|
351
|
+
ok(id, result) {
|
|
352
|
+
return { jsonrpc: "2.0", id, result };
|
|
353
|
+
}
|
|
354
|
+
err(id, code, message) {
|
|
355
|
+
return { jsonrpc: "2.0", id, error: { code, message } };
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
function isRequestShaped(message) {
|
|
359
|
+
if (typeof message !== "object" || message === null) return false;
|
|
360
|
+
const m = message;
|
|
361
|
+
if (m["jsonrpc"] !== "2.0") return false;
|
|
362
|
+
if (typeof m["method"] !== "string") return false;
|
|
363
|
+
const id = m["id"];
|
|
364
|
+
return id === void 0 || id === null || typeof id === "string" || typeof id === "number";
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// src/cli.ts
|
|
368
|
+
function main() {
|
|
369
|
+
let server;
|
|
370
|
+
try {
|
|
371
|
+
server = new BlursecMcpServer(createBlursecFromEnv());
|
|
372
|
+
} catch (err) {
|
|
373
|
+
process.stderr.write(
|
|
374
|
+
`blursec-mcp: ${err instanceof Error ? err.message : String(err)}
|
|
375
|
+
`
|
|
376
|
+
);
|
|
377
|
+
process.exitCode = 1;
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
const rl = createInterface({ input: process.stdin, terminal: false });
|
|
381
|
+
let pipeline = Promise.resolve();
|
|
382
|
+
rl.on("line", (line) => {
|
|
383
|
+
if (line.trim().length === 0) return;
|
|
384
|
+
pipeline = pipeline.then(async () => {
|
|
385
|
+
try {
|
|
386
|
+
const reply = await server.handleLine(line);
|
|
387
|
+
if (reply !== void 0) process.stdout.write(reply + "\n");
|
|
388
|
+
} catch (err) {
|
|
389
|
+
process.stderr.write(
|
|
390
|
+
`blursec-mcp: internal error: ${err instanceof Error ? err.message : String(err)}
|
|
391
|
+
`
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
rl.on("close", () => {
|
|
397
|
+
void pipeline.then(() => {
|
|
398
|
+
process.exitCode = 0;
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
main();
|
|
403
|
+
//# sourceMappingURL=cli.js.map
|
|
404
|
+
//# sourceMappingURL=cli.js.map
|