webui: MCP Diagnostics improvements (#21803)
* Add MCP Connection diagnostics and CORS hint to web-ui * tidy up test * webui: Refactor and improve MCP diagnostic logging --------- Co-authored-by: evalstate <1936278+evalstate@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
bafae27654
commit
227ed28e12
@@ -1,4 +1,6 @@
|
||||
import { config } from '$lib/stores/settings.svelte';
|
||||
import { REDACTED_HEADERS } from '$lib/constants';
|
||||
import { redactValue } from './redact';
|
||||
|
||||
/**
|
||||
* Get authorization headers for API requests
|
||||
@@ -20,3 +22,46 @@ export function getJsonHeaders(): Record<string, string> {
|
||||
...getAuthHeaders()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize HTTP headers by redacting sensitive values.
|
||||
* Known sensitive headers (from REDACTED_HEADERS) and any extra headers
|
||||
* specified by the caller are fully redacted. Headers listed in
|
||||
* `partialRedactHeaders` are partially redacted, showing only the
|
||||
* specified number of trailing characters.
|
||||
*
|
||||
* @param headers - Headers to sanitize
|
||||
* @param extraRedactedHeaders - Additional header names to fully redact
|
||||
* @param partialRedactHeaders - Map of header name -> number of trailing chars to keep visible
|
||||
* @returns Object with header names as keys and (possibly redacted) values
|
||||
*/
|
||||
export function sanitizeHeaders(
|
||||
headers?: HeadersInit,
|
||||
extraRedactedHeaders?: Iterable<string>,
|
||||
partialRedactHeaders?: Map<string, number>
|
||||
): Record<string, string> {
|
||||
if (!headers) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const normalized = new Headers(headers);
|
||||
const sanitized: Record<string, string> = {};
|
||||
const redactedHeaders = new Set(
|
||||
Array.from(extraRedactedHeaders ?? [], (header) => header.toLowerCase())
|
||||
);
|
||||
|
||||
for (const [key, value] of normalized.entries()) {
|
||||
const normalizedKey = key.toLowerCase();
|
||||
const partialChars = partialRedactHeaders?.get(normalizedKey);
|
||||
|
||||
if (partialChars !== undefined) {
|
||||
sanitized[key] = redactValue(value, partialChars);
|
||||
} else if (REDACTED_HEADERS.has(normalizedKey) || redactedHeaders.has(normalizedKey)) {
|
||||
sanitized[key] = redactValue(value);
|
||||
} else {
|
||||
sanitized[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
// API utilities
|
||||
export { getAuthHeaders, getJsonHeaders } from './api-headers';
|
||||
export { getAuthHeaders, getJsonHeaders, sanitizeHeaders } from './api-headers';
|
||||
export { apiFetch, apiFetchWithParams, apiPost, type ApiFetchOptions } from './api-fetch';
|
||||
export { validateApiKey } from './api-key-validation';
|
||||
|
||||
@@ -164,6 +164,20 @@ export { runLegacyMigration, isMigrationNeeded } from './legacy-migration';
|
||||
// Cache utilities
|
||||
export { TTLCache, ReactiveTTLMap, type TTLCacheOptions } from './cache-ttl';
|
||||
|
||||
// Redaction utilities
|
||||
export { redactValue } from './redact';
|
||||
|
||||
// Request inspection utilities
|
||||
export {
|
||||
getRequestUrl,
|
||||
getRequestMethod,
|
||||
getRequestBody,
|
||||
summarizeRequestBody,
|
||||
formatDiagnosticErrorMessage,
|
||||
extractJsonRpcMethods,
|
||||
type RequestBodySummary
|
||||
} from './request-helpers';
|
||||
|
||||
// Abort signal utilities
|
||||
export {
|
||||
throwIfAborted,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Redacts a sensitive value, optionally showing the last N characters.
|
||||
*
|
||||
* @param value - The value to redact
|
||||
* @param showLastChars - If provided, reveals the last N characters with a leading mask
|
||||
* @returns The redacted string
|
||||
*/
|
||||
export function redactValue(value: string, showLastChars?: number): string {
|
||||
if (showLastChars) {
|
||||
return `....${value.slice(-showLastChars)}`;
|
||||
}
|
||||
|
||||
return '[redacted]';
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* HTTP request inspection utilities for diagnostic logging.
|
||||
* These helpers extract metadata from fetch-style request arguments
|
||||
* without exposing sensitive payload data.
|
||||
*/
|
||||
|
||||
export interface RequestBodySummary {
|
||||
kind: string;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export function getRequestUrl(input: RequestInfo | URL): string {
|
||||
if (typeof input === 'string') {
|
||||
return input;
|
||||
}
|
||||
|
||||
if (input instanceof URL) {
|
||||
return input.href;
|
||||
}
|
||||
|
||||
return input.url;
|
||||
}
|
||||
|
||||
export function getRequestMethod(
|
||||
input: RequestInfo | URL,
|
||||
init?: RequestInit,
|
||||
baseInit?: RequestInit
|
||||
): string {
|
||||
if (init?.method) {
|
||||
return init.method;
|
||||
}
|
||||
|
||||
if (typeof Request !== 'undefined' && input instanceof Request) {
|
||||
return input.method;
|
||||
}
|
||||
|
||||
return baseInit?.method ?? 'GET';
|
||||
}
|
||||
|
||||
export function getRequestBody(
|
||||
input: RequestInfo | URL,
|
||||
init?: RequestInit
|
||||
): BodyInit | null | undefined {
|
||||
if (init?.body !== undefined) {
|
||||
return init.body;
|
||||
}
|
||||
|
||||
if (typeof Request !== 'undefined' && input instanceof Request) {
|
||||
return input.body;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function summarizeRequestBody(body: BodyInit | null | undefined): RequestBodySummary {
|
||||
if (body == null) {
|
||||
return { kind: 'empty' };
|
||||
}
|
||||
|
||||
if (typeof body === 'string') {
|
||||
return { kind: 'string', size: body.length };
|
||||
}
|
||||
|
||||
if (body instanceof Blob) {
|
||||
return { kind: 'blob', size: body.size };
|
||||
}
|
||||
|
||||
if (body instanceof URLSearchParams) {
|
||||
return { kind: 'urlsearchparams', size: body.toString().length };
|
||||
}
|
||||
|
||||
if (body instanceof FormData) {
|
||||
return { kind: 'formdata' };
|
||||
}
|
||||
|
||||
if (body instanceof ArrayBuffer) {
|
||||
return { kind: 'arraybuffer', size: body.byteLength };
|
||||
}
|
||||
|
||||
if (ArrayBuffer.isView(body)) {
|
||||
return { kind: body.constructor.name, size: body.byteLength };
|
||||
}
|
||||
|
||||
return { kind: typeof body };
|
||||
}
|
||||
|
||||
export function formatDiagnosticErrorMessage(error: unknown): string {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
|
||||
return message.includes('Failed to fetch') ? `${message} (check CORS?)` : message;
|
||||
}
|
||||
|
||||
export function extractJsonRpcMethods(body: BodyInit | null | undefined): string[] | undefined {
|
||||
if (typeof body !== 'string') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(body);
|
||||
const messages = Array.isArray(parsed) ? parsed : [parsed];
|
||||
const methods = messages
|
||||
.map((message: Record<string, unknown>) =>
|
||||
typeof message?.method === 'string' ? (message.method as string) : undefined
|
||||
)
|
||||
.filter((method: string | undefined): method is string => Boolean(method));
|
||||
|
||||
return methods.length > 0 ? methods : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user