UNPKG

@centure/node-sdk

Version:

A Typescript SDK for interacting with Centure's API

261 lines 13.3 kB
"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