@centure/node-sdk
Version:
A Typescript SDK for interacting with Centure's API
261 lines • 13.3 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CentureMCPClientTransport = exports.CENTURE_SECURITY_VIOLATION_ERROR = void 0;
const is_image_js_1 = require("../utils/is-image.js");
/**
* JSONRPC error code for security violations detected by Centure scan.
* Falls within the implementation-specific server error range (-32000 to -32099).
*/
exports.CENTURE_SECURITY_VIOLATION_ERROR = -32001;
class CentureMCPClientTransport {
constructor(options) {
this.wrappedTransport = options.transport;
this.client = options.client;
this.shouldScanMessageHook = options.shouldScanMessage;
this.onAfterScanHook = options.onAfterScan;
this.onUnsafeMessageHook = options.onUnsafeMessage;
}
/**
* Creates a JSONRPC error response for an unsafe message
* For tools/call, returns CallToolResult format with isError flag
* For all other methods, returns standard JSON-RPC error format
*/
createErrorResponse(requestId, method, scanResult) {
// For tools/call, use CallToolResult format with isError
if (method === "tools/call") {
return {
jsonrpc: "2.0",
id: requestId,
result: {
isError: true,
content: [
{
type: "text",
text: JSON.stringify({
code: exports.CENTURE_SECURITY_VIOLATION_ERROR,
message: "Message blocked by Centure security scan",
data: {
categories: scanResult.categories,
reason: "Unsafe content detected",
},
}),
},
],
},
};
}
// For all other methods (tools/list, resources/list, etc.), use standard JSON-RPC error format
return {
jsonrpc: "2.0",
id: requestId,
error: {
code: exports.CENTURE_SECURITY_VIOLATION_ERROR,
message: "Message blocked by Centure security scan",
data: {
categories: scanResult.categories,
reason: "Unsafe content detected",
},
},
};
}
/**
* Extracts content from a JSONRPC message for scanning
* Returns arrays of text and image content to scan separately
*/
extractContentForScanning(message) {
var _a;
const texts = [];
const images = [];
// Check if this is a request or response with content to scan
if ("result" in message && message.result) {
const result = message.result;
if (result.content && Array.isArray(result.content)) {
// MCP content array format
for (const item of result.content) {
if (item.type === "text" && item.text) {
texts.push(item.text);
}
else if (item.type === "image" && item.data) {
images.push(item.data);
}
else if (item.type === "resource" &&
((_a = item.resource) === null || _a === void 0 ? void 0 : _a.blob) &&
(0, is_image_js_1.isImageMimeType)(item.resource.mimeType)) {
images.push(item.resource.blob);
}
}
}
else if (result.content && typeof result.content === "string") {
// Plain string content
texts.push(result.content);
}
else {
// Fallback: stringify the entire result as text
texts.push(JSON.stringify(result));
}
}
else if ("params" in message && message.params) {
// For requests, scan the params as text
texts.push(JSON.stringify(message.params));
}
return { texts, images };
}
start() {
return __awaiter(this, void 0, void 0, function* () {
// Wire up callbacks from the wrapped transport to our callbacks
this.wrappedTransport.onclose = () => {
var _a;
(_a = this.onclose) === null || _a === void 0 ? void 0 : _a.call(this);
};
this.wrappedTransport.onerror = (error) => {
var _a;
(_a = this.onerror) === null || _a === void 0 ? void 0 : _a.call(this, error);
};
this.wrappedTransport.onmessage = (message, extra) => __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
try {
// Step 1: Check if we should scan this message
let shouldScan = true;
if (this.shouldScanMessageHook) {
shouldScan = yield this.shouldScanMessageHook({
message,
extra,
});
}
// If we're not scanning, pass through immediately
if (!shouldScan) {
(_a = this.onmessage) === null || _a === void 0 ? void 0 : _a.call(this, message, extra);
return;
}
// Step 2: Extract content and perform the scan
const { texts, images } = this.extractContentForScanning(message);
// If there's no content to scan, pass through
if (texts.length === 0 && images.length === 0) {
(_b = this.onmessage) === null || _b === void 0 ? void 0 : _b.call(this, message, extra);
return;
}
// Scan all text and image content
const scanPromises = [];
for (const text of texts) {
scanPromises.push(this.client.scanText(text));
}
for (const image of images) {
scanPromises.push(this.client.scanImage(image));
}
const scanResults = yield Promise.all(scanPromises);
// Combine scan results - message is unsafe if ANY scan is unsafe
const isUnsafe = scanResults.some((result) => !result.is_safe);
const allCategories = scanResults.flatMap((result) => result.categories);
// Create a combined scan result for hooks
const combinedScanResult = {
is_safe: !isUnsafe,
categories: allCategories,
request_id: ((_c = scanResults[0]) === null || _c === void 0 ? void 0 : _c.request_id) || "",
api_key_id: ((_d = scanResults[0]) === null || _d === void 0 ? void 0 : _d.api_key_id) || "",
request_units: scanResults.reduce((sum, result) => sum + result.request_units, 0),
billed_request_units: scanResults.reduce((sum, result) => sum + result.billed_request_units, 0),
service_tier: ((_e = scanResults[0]) === null || _e === void 0 ? void 0 : _e.service_tier) || "standard",
};
// Step 3: Call onAfterScan hook if provided
if (this.onAfterScanHook) {
const afterScanResult = yield this.onAfterScanHook({
message,
extra,
scanResult: combinedScanResult,
});
// If the hook says to pass through, do so without further checks
if (afterScanResult.passthrough) {
(_f = this.onmessage) === null || _f === void 0 ? void 0 : _f.call(this, message, extra);
return;
}
}
// Step 4: Handle unsafe messages
if (!combinedScanResult.is_safe) {
let messageToForward = null;
// Call onUnsafeMessage hook if provided
if (this.onUnsafeMessageHook) {
const unsafeResult = yield this.onUnsafeMessageHook({
message,
extra,
scanResult: combinedScanResult,
});
if (unsafeResult.passthrough) {
// User chose to allow the message through
messageToForward = (_g = unsafeResult.replace) !== null && _g !== void 0 ? _g : message;
}
else if (unsafeResult.replace) {
// User provided a replacement message
messageToForward = unsafeResult.replace;
}
else {
// passthrough is false and no replacement provided
// Send JSONRPC error to inform client of blocked attack
if ("id" in message && message.id !== undefined) {
messageToForward = this.createErrorResponse(message.id, "method" in message ? message.method : undefined, combinedScanResult);
}
// If it's a notification or response, just drop it (no error response needed)
}
}
else {
// No hook provided, default behavior: block and send error
if ("id" in message && message.id !== undefined) {
messageToForward = this.createErrorResponse(message.id, "method" in message ? message.method : undefined, combinedScanResult);
}
// If it's a notification or response, just drop it (no error response needed)
}
// Forward the message if we have one
if (messageToForward) {
(_h = this.onmessage) === null || _h === void 0 ? void 0 : _h.call(this, messageToForward, extra);
}
}
else {
// Message is safe, forward as-is
(_j = this.onmessage) === null || _j === void 0 ? void 0 : _j.call(this, message, extra);
}
}
catch (error) {
if (error instanceof Error) {
// If scanning fails, report error but pass through the message
(_k = this.onerror) === null || _k === void 0 ? void 0 : _k.call(this, error);
}
throw error;
}
});
// Forward setProtocolVersion if it exists
if (this.wrappedTransport.setProtocolVersion) {
const originalSetProtocolVersion = this.wrappedTransport.setProtocolVersion.bind(this.wrappedTransport);
this.setProtocolVersion = (version) => {
originalSetProtocolVersion(version);
};
}
// Delegate to wrapped transport's start
yield this.wrappedTransport.start();
// Copy sessionId if available
if (this.wrappedTransport.sessionId) {
this.sessionId = this.wrappedTransport.sessionId;
}
});
}
send(message, options) {
return __awaiter(this, void 0, void 0, function* () {
// Forward messages from client to server without scanning
yield this.wrappedTransport.send(message, options);
});
}
close() {
return __awaiter(this, void 0, void 0, function* () {
yield this.wrappedTransport.close();
});
}
}
exports.CentureMCPClientTransport = CentureMCPClientTransport;
//# sourceMappingURL=centure-mcp-client-transport.js.map