chat-ui / src /lib /utils /mcpValidation.ts
victor's picture
victor HF Staff
MCP (#1981)
e67ab0e unverified
raw
history blame
3.4 kB
/**
* URL validation and sanitization utilities for MCP integration
*/
import { browser } from "$app/environment";
import { dev } from "$app/environment";
/**
* Sanitize and validate a URL for MCP server connections
* @param urlString - The URL string to validate
* @returns Sanitized URL string or null if invalid
*/
export function validateMcpServerUrl(urlString: string): string | null {
if (!urlString || typeof urlString !== "string") {
return null;
}
try {
const url = new URL(urlString.trim());
// Allow http/https only
if (!["http:", "https:"].includes(url.protocol)) {
return null;
}
// Warn about non-HTTPS in production
if (!dev && url.protocol === "http:" && browser) {
console.warn(
"Warning: Connecting to non-HTTPS MCP server in production. This may expose sensitive data."
);
}
// Block certain localhost/private IPs in production
if (!dev && isPrivateOrLocalhost(url.hostname)) {
console.warn("Warning: Localhost/private IP addresses are not recommended in production.");
}
return url.toString();
} catch (error) {
// Invalid URL
return null;
}
}
/**
* Check if hostname is localhost or a private IP
*/
function isPrivateOrLocalhost(hostname: string): boolean {
// Localhost checks
if (
hostname === "localhost" ||
hostname === "127.0.0.1" ||
hostname === "::1" ||
hostname.endsWith(".localhost")
) {
return true;
}
// Private IP ranges (IPv4)
const ipv4Regex = /^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|127\.|0\.0\.0\.0|169\.254\.)/;
if (ipv4Regex.test(hostname)) {
return true;
}
return false;
}
/**
* Sanitize URL by removing sensitive parts
* Used for logging and display purposes
*/
export function sanitizeUrlForDisplay(urlString: string): string {
try {
const url = new URL(urlString);
// Remove username/password if present
url.username = "";
url.password = "";
return url.toString();
} catch {
return urlString;
}
}
/**
* Check if URL is safe to connect to
* Returns an error message if unsafe, null if safe
*/
export function checkUrlSafety(urlString: string): string | null {
const validated = validateMcpServerUrl(urlString);
if (!validated) {
return "Invalid URL. Please use http:// or https:// URLs only.";
}
try {
const url = new URL(validated);
// Additional safety checks
if (!dev && url.protocol === "http:") {
return "Non-HTTPS URLs are not recommended in production. Please use https:// for security.";
}
return null; // Safe
} catch {
return "Invalid URL format.";
}
}
/**
* Check if a header key is likely to contain sensitive data
*/
export function isSensitiveHeader(key: string): boolean {
const sensitiveKeys = [
"authorization",
"api-key",
"api_key",
"apikey",
"token",
"secret",
"password",
"bearer",
"x-api-key",
"x-auth-token",
];
const lowerKey = key.toLowerCase();
return sensitiveKeys.some((sensitive) => lowerKey.includes(sensitive));
}
/**
* Validate header key-value pair
* Returns error message if invalid, null if valid
*/
export function validateHeader(key: string, value: string): string | null {
if (!key || !key.trim()) {
return "Header name is required";
}
if (!/^[a-zA-Z0-9_-]+$/.test(key)) {
return "Header name can only contain letters, numbers, hyphens, and underscores";
}
if (!value) {
return "Header value is required";
}
return null;
}