UNPKG

@himorishige/noren-core

Version:

Core PII detection, masking, and tokenization library built on Web Standards

187 lines (186 loc) 6.36 kB
// MCP (Model Context Protocol) specific utilities for stdio communication import { redactText } from './index.js'; /** * Parse line-delimited JSON messages commonly used in MCP */ export function parseJsonLines(text) { const messages = []; const lines = text.split('\n').filter((line) => line.trim()); for (const line of lines) { try { const parsed = JSON.parse(line.trim()); if (isValidJsonRpcMessage(parsed)) { messages.push(parsed); } } catch { } } return messages; } /** * Validate JSON-RPC message structure */ export function isValidJsonRpcMessage(obj) { if (!obj || typeof obj !== 'object') return false; const msg = obj; // Must have jsonrpc field with value "2.0" if (msg.jsonrpc !== '2.0') return false; // Must have either method (request) or result/error (response) const hasMethod = typeof msg.method === 'string'; const hasResult = 'result' in msg; const hasError = 'error' in msg; return hasMethod || hasResult || hasError; } /** * Extract potentially sensitive content from JSON-RPC message */ export function extractSensitiveContent(message) { const content = []; // Extract from params (requests) if (message.params) { const paramsStr = JSON.stringify(message.params); content.push(paramsStr); } // Extract from result (successful responses) if (message.result) { const resultStr = JSON.stringify(message.result); content.push(resultStr); } // Extract from error data (error responses) if (message.error?.data) { const errorStr = JSON.stringify(message.error.data); content.push(errorStr); } return content; } /** * Redact PII from JSON-RPC message while preserving structure */ export async function redactJsonRpcMessage(message, options) { const { registry, policy = {} } = options; // Create a deep copy to avoid mutating original const redacted = JSON.parse(JSON.stringify(message)); // Redact params if present if (redacted.params) { const paramsText = JSON.stringify(redacted.params); const redactedParams = await redactText(registry, paramsText, policy); try { redacted.params = JSON.parse(redactedParams); } catch { // If redaction broke JSON structure, use string representation redacted.params = redactedParams; } } // Redact result if present if (redacted.result) { const resultText = JSON.stringify(redacted.result); const redactedResult = await redactText(registry, resultText, policy); try { redacted.result = JSON.parse(redactedResult); } catch { redacted.result = redactedResult; } } // Redact error data if present if (redacted.error?.data) { const errorText = JSON.stringify(redacted.error.data); const redactedError = await redactText(registry, errorText, policy); try { redacted.error.data = JSON.parse(redactedError); } catch { redacted.error.data = redactedError; } } return redacted; } /** * Create a transform stream for MCP line-delimited JSON processing */ export function createMCPRedactionTransform(options) { const { registry, policy = {}, lineBufferSize = 64 * 1024 } = options; let buffer = ''; const decoder = new TextDecoder(); const encoder = new TextEncoder(); return new TransformStream({ async transform(chunk, controller) { // Decode chunk and add to buffer const text = decoder.decode(chunk, { stream: true }); buffer += text; // Process complete lines const lines = buffer.split('\n'); buffer = lines.pop() || ''; // Keep incomplete line in buffer for (const line of lines) { if (line.trim()) { await processLine(line.trim(), controller); } else { // Preserve empty lines controller.enqueue(encoder.encode('\n')); } } // Prevent buffer from growing too large if (buffer.length > lineBufferSize) { await processLine(buffer, controller); buffer = ''; } }, async flush(controller) { // Process any remaining buffer content if (buffer.trim()) { await processLine(buffer.trim(), controller); } }, }); async function processLine(line, controller) { try { const message = JSON.parse(line); if (isValidJsonRpcMessage(message)) { // Process as JSON-RPC message const redacted = await redactJsonRpcMessage(message, { registry, policy }); const output = `${JSON.stringify(redacted)}\n`; controller.enqueue(encoder.encode(output)); } else { // Process as plain text const redacted = await redactText(registry, line, policy); controller.enqueue(encoder.encode(`${redacted}\n`)); } } catch { // If JSON parsing fails, process as plain text const redacted = await redactText(registry, line, policy); controller.enqueue(encoder.encode(`${redacted}\n`)); } } } /** * Utility to check if text contains JSON-RPC patterns */ export function containsJsonRpcPattern(text) { return text.includes('"jsonrpc":"2.0"') || text.includes('"jsonrpc": "2.0"'); } /** * Extract method names from JSON-RPC requests for logging/monitoring */ export function extractMethodName(message) { return message.method || null; } /** * Check if message is a request, response, or notification */ export function getMessageType(message) { if (message.error) return 'error'; if (message.method && message.id !== undefined) return 'request'; if (message.method && message.id === undefined) return 'notification'; if ('result' in message || 'error' in message) return 'response'; return 'request'; // fallback }