diff --git a/packages/codegen/src/watcher.ts b/packages/codegen/src/watcher.ts index ede8b7ac..2d84656a 100644 --- a/packages/codegen/src/watcher.ts +++ b/packages/codegen/src/watcher.ts @@ -18,19 +18,32 @@ import { hasValidExtension, shouldSkipFile } from "./naming"; import { getVersionDirectories } from "./generator"; /** - * Default logger + * Default logger backed by structured logging with timestamp and source prefix. + * Respects MCP_APPS_LOG_LEVEL environment variable (default: "info"). */ -const defaultLogger: PluginLogger = { - info: (message: string) => { - console.log(`[mcp-apps-plugin] ${message}`); - }, - warn: (message: string) => { - console.warn(`[mcp-apps-plugin] ${message}`); - }, - error: (message: string) => { - console.error(`[mcp-apps-plugin] ${message}`); - }, -}; +const defaultLogger: PluginLogger = (() => { + const source = "codegen:watcher"; + const ts = () => new Date().toISOString(); + const LEVELS: Record = { debug: 0, info: 1, warn: 2, error: 3, silent: 4 }; + const envLevel = + typeof process !== "undefined" ? (process.env.MCP_APPS_LOG_LEVEL ?? "").toLowerCase() : ""; + const threshold: number = LEVELS[envLevel] ?? 1; // 1 = info + const ok = (l: string): boolean => (LEVELS[l] ?? 1) >= threshold; + return { + info: (message: string) => { + // eslint-disable-next-line no-console + if (ok("info")) console.info(`${ts()} [INFO] [${source}]`, message); + }, + warn: (message: string) => { + // eslint-disable-next-line no-console + if (ok("warn")) console.warn(`${ts()} [WARN] [${source}]`, message); + }, + error: (message: string) => { + // eslint-disable-next-line no-console + if (ok("error")) console.error(`${ts()} [ERROR] [${source}]`, message); + }, + }; +})(); /** * Options for setting up the watcher diff --git a/packages/core/tests/unit/utils/csp.test.ts b/packages/core/tests/unit/utils/csp.test.ts new file mode 100644 index 00000000..e81176f1 --- /dev/null +++ b/packages/core/tests/unit/utils/csp.test.ts @@ -0,0 +1,148 @@ +import { describe, it, expect } from "vitest"; +import { + generateMcpCSPMetadata, + generateOpenAICSPMetadata, + generateMcpUIMetadata, + generateOpenAIUIMetadata, +} from "../../../src/utils/csp"; +import type { CSPConfig, UIDef } from "../../../src/types/ui"; + +describe("generateMcpCSPMetadata", () => { + it("returns empty object for empty config", () => { + expect(generateMcpCSPMetadata({})).toEqual({}); + }); + + it("includes connectDomains when present", () => { + const csp: CSPConfig = { connectDomains: ["https://api.example.com"] }; + expect(generateMcpCSPMetadata(csp)).toEqual({ + connectDomains: ["https://api.example.com"], + }); + }); + + it("includes resourceDomains when present", () => { + const csp: CSPConfig = { resourceDomains: ["https://cdn.example.com"] }; + expect(generateMcpCSPMetadata(csp)).toEqual({ + resourceDomains: ["https://cdn.example.com"], + }); + }); + + it("ignores ChatGPT-only fields (redirectDomains, frameDomains)", () => { + const csp: CSPConfig = { + connectDomains: ["https://api.example.com"], + redirectDomains: ["https://docs.example.com"], + frameDomains: ["https://embed.example.com"], + }; + expect(generateMcpCSPMetadata(csp)).toEqual({ + connectDomains: ["https://api.example.com"], + }); + }); + + it("omits empty arrays", () => { + const csp: CSPConfig = { connectDomains: [], resourceDomains: [] }; + expect(generateMcpCSPMetadata(csp)).toEqual({}); + }); +}); + +describe("generateOpenAICSPMetadata", () => { + it("returns empty object for empty config", () => { + expect(generateOpenAICSPMetadata({})).toEqual({}); + }); + + it("maps all four CSP fields to snake_case", () => { + const csp: CSPConfig = { + connectDomains: ["https://api.example.com"], + resourceDomains: ["https://cdn.example.com"], + redirectDomains: ["https://docs.example.com"], + frameDomains: ["https://embed.example.com"], + }; + expect(generateOpenAICSPMetadata(csp)).toEqual({ + connect_domains: ["https://api.example.com"], + resource_domains: ["https://cdn.example.com"], + redirect_domains: ["https://docs.example.com"], + frame_domains: ["https://embed.example.com"], + }); + }); + + it("omits empty arrays", () => { + const csp: CSPConfig = { connectDomains: [], frameDomains: [] }; + expect(generateOpenAICSPMetadata(csp)).toEqual({}); + }); +}); + +describe("generateMcpUIMetadata", () => { + it("generates minimal metadata with key as name fallback", () => { + const uiDef: UIDef = { html: "
Hello
" }; + const result = generateMcpUIMetadata("widget", uiDef); + expect(result).toEqual({ + name: "widget", + html: "
Hello
", + }); + }); + + it("uses explicit name over key", () => { + const uiDef: UIDef = { html: "
", name: "My Widget" }; + expect(generateMcpUIMetadata("widget", uiDef).name).toBe("My Widget"); + }); + + it("includes description when provided", () => { + const uiDef: UIDef = { html: "
", description: "A widget" }; + expect(generateMcpUIMetadata("w", uiDef).description).toBe("A widget"); + }); + + it("includes CSP when non-empty", () => { + const uiDef: UIDef = { + html: "
", + csp: { connectDomains: ["https://api.example.com"] }, + }; + const result = generateMcpUIMetadata("w", uiDef); + expect(result.csp).toEqual({ connectDomains: ["https://api.example.com"] }); + }); + + it("omits CSP when all arrays are empty", () => { + const uiDef: UIDef = { html: "
", csp: { connectDomains: [] } }; + const result = generateMcpUIMetadata("w", uiDef); + expect(result.csp).toBeUndefined(); + }); + + it("includes prefersBorder when set", () => { + const uiDef: UIDef = { html: "
", prefersBorder: true }; + expect(generateMcpUIMetadata("w", uiDef).prefersBorder).toBe(true); + }); +}); + +describe("generateOpenAIUIMetadata", () => { + it("generates minimal metadata", () => { + const uiDef: UIDef = { html: "
Hello
" }; + const result = generateOpenAIUIMetadata("widget", uiDef); + expect(result).toEqual({ + name: "widget", + html: "
Hello
", + }); + }); + + it("includes openai/widgetCSP with snake_case fields", () => { + const uiDef: UIDef = { + html: "
", + csp: { + connectDomains: ["https://api.example.com"], + frameDomains: ["https://embed.example.com"], + }, + }; + const result = generateOpenAIUIMetadata("w", uiDef); + expect(result["openai/widgetCSP"]).toEqual({ + connect_domains: ["https://api.example.com"], + frame_domains: ["https://embed.example.com"], + }); + }); + + it("includes domain when provided", () => { + const uiDef: UIDef = { html: "
", domain: "https://app.example.com" }; + expect(generateOpenAIUIMetadata("w", uiDef).domain).toBe("https://app.example.com"); + }); + + it("omits widgetCSP when all arrays are empty", () => { + const uiDef: UIDef = { html: "
", csp: { resourceDomains: [] } }; + const result = generateOpenAIUIMetadata("w", uiDef); + expect(result["openai/widgetCSP"]).toBeUndefined(); + }); +}); diff --git a/packages/inspector/src/connection.ts b/packages/inspector/src/connection.ts index 60bf98fa..822776dd 100644 --- a/packages/inspector/src/connection.ts +++ b/packages/inspector/src/connection.ts @@ -31,6 +31,9 @@ import { WidgetServer } from "./widget-server"; import { InspectorOAuthProvider } from "./oauth/provider"; import type { OAuthState } from "./oauth/types"; import { discoverAuthRequirements, type AuthRequiredEvent } from "./oauth/discovery"; +import { createLogger } from "./debug/logger"; + +const logger = createLogger("connection"); /** * Protocol type inferred from connected server's tools @@ -224,7 +227,7 @@ export class ConnectionManager extends EventEmitter { const existingProvider = this.oauthProvider; if (this.state.connected && this.state.client) { if (this.debug) { - console.log(`[inspector] Disconnecting from previous server: ${this.state.serverUrl}`); + logger.info(`[inspector] Disconnecting from previous server: ${this.state.serverUrl}`); } // Temporarily clear provider so disconnect() doesn't revoke tokens we still need this.oauthProvider = null; @@ -236,7 +239,7 @@ export class ConnectionManager extends EventEmitter { } if (this.debug) { - console.log(`[inspector] Connecting to server: ${label}`); + logger.info(`[inspector] Connecting to server: ${label}`); } // Wire onTransportClose for stdio auto-restart @@ -259,7 +262,7 @@ export class ConnectionManager extends EventEmitter { provider.onStatusChange = () => { if (this.debug) { const state = provider.getOAuthState(); - console.log(`[inspector] OAuth status changed: ${state.status}`); + logger.info(`[inspector] OAuth status changed: ${state.status}`); } }; @@ -287,7 +290,7 @@ export class ConnectionManager extends EventEmitter { provider.onStatusChange = () => { if (this.debug) { const state = provider.getOAuthState(); - console.log(`[inspector] OAuth status changed: ${state.status}`); + logger.info(`[inspector] OAuth status changed: ${state.status}`); } }; @@ -309,7 +312,7 @@ export class ConnectionManager extends EventEmitter { // Check if this is an auth error on an HTTP connection without OAuth configured if (params.transport === "http" && !authProvider && !oauthConfig && isAuthError(error)) { if (this.debug) { - console.log(`[inspector] Auth error detected during connect, running discovery`); + logger.info(`[inspector] Auth error detected during connect, running discovery`); } // Run discovery and emit authRequired instead of throwing @@ -343,7 +346,7 @@ export class ConnectionManager extends EventEmitter { const pendingUrl = authProvider.getPendingAuthUrl?.(); if (pendingUrl || this.oauthProvider) { if (this.debug) { - console.log( + logger.info( `[inspector] Auth error with OAuth configured, pending auth URL: ${pendingUrl}` ); } @@ -383,7 +386,7 @@ export class ConnectionManager extends EventEmitter { if (params.transport === "http" && !authProvider && !oauthConfig && isAuthError(error)) { // Auth error during capability listing (server accepted transport but rejected request) if (this.debug) { - console.log(`[inspector] Auth error during capability listing, running discovery`); + logger.info(`[inspector] Auth error during capability listing, running discovery`); } const discovery = await discoverAuthRequirements(params.url); @@ -418,7 +421,7 @@ export class ConnectionManager extends EventEmitter { // Server doesn't support tools capability (non-auth error) if (this.debug) { - console.log(`[inspector] Server doesn't support tools capability`); + logger.info(`[inspector] Server doesn't support tools capability`); } } @@ -427,7 +430,7 @@ export class ConnectionManager extends EventEmitter { } catch { // Server doesn't support resources capability if (this.debug) { - console.log(`[inspector] Server doesn't support resources capability`); + logger.info(`[inspector] Server doesn't support resources capability`); } } @@ -436,7 +439,7 @@ export class ConnectionManager extends EventEmitter { } catch { // Server doesn't support prompts capability if (this.debug) { - console.log(`[inspector] Server doesn't support prompts capability`); + logger.info(`[inspector] Server doesn't support prompts capability`); } } @@ -479,8 +482,8 @@ export class ConnectionManager extends EventEmitter { this.autoRestartAttempts = 0; if (this.debug) { - console.log(`[inspector] Connected to ${label}`); - console.log( + logger.info(`[inspector] Connected to ${label}`); + logger.info( `[inspector] Tools: ${tools.length}, Resources: ${resources.length}, Prompts: ${prompts.length}` ); } @@ -583,7 +586,7 @@ export class ConnectionManager extends EventEmitter { await this.state.client.disconnect(); } catch (error) { if (this.debug) { - console.warn(`[inspector] Error during disconnect:`, error); + logger.warn(`[inspector] Error during disconnect:`, error); } } } @@ -610,7 +613,7 @@ export class ConnectionManager extends EventEmitter { // Server-side token revocation (fire-and-forget) this.oauthProvider.revokeTokens().catch((err: unknown) => { if (this.debug) { - console.warn(`[inspector] Token revocation during disconnect failed:`, err); + logger.warn(`[inspector] Token revocation during disconnect failed:`, err); } }); // Delete persisted token file so next connect requires fresh login @@ -628,7 +631,7 @@ export class ConnectionManager extends EventEmitter { this.oauthProvider = null; if (this.debug) { - console.log(`[inspector] Disconnected from ${previousUrl}`); + logger.info(`[inspector] Disconnected from ${previousUrl}`); } // Emit disconnected event for proxy cleanup @@ -643,7 +646,7 @@ export class ConnectionManager extends EventEmitter { private handleStdioProcessExit(params: ConnectionParams, options: ConnectOptions): void { if (this.autoRestartAttempts >= ConnectionManager.MAX_RESTART_ATTEMPTS) { if (this.debug) { - console.log( + logger.info( `[inspector] Max auto-restart attempts (${ConnectionManager.MAX_RESTART_ATTEMPTS}) reached, disconnecting` ); } @@ -655,7 +658,7 @@ export class ConnectionManager extends EventEmitter { this.autoRestartAttempts++; if (this.debug) { - console.log( + logger.info( `[inspector] stdio process exited, restarting in ${delay}ms (attempt ${this.autoRestartAttempts}/${ConnectionManager.MAX_RESTART_ATTEMPTS})` ); } @@ -668,7 +671,7 @@ export class ConnectionManager extends EventEmitter { // Abort if disconnect() was called while we were waiting if (this.connectionGeneration !== generationAtStart) { if (this.debug) { - console.log(`[inspector] Auto-restart aborted: disconnect called during backoff`); + logger.info(`[inspector] Auto-restart aborted: disconnect called during backoff`); } return; } @@ -678,7 +681,7 @@ export class ConnectionManager extends EventEmitter { // Abort if disconnect() was called while connect() was in-flight if (this.connectionGeneration !== generationAtStart) { if (this.debug) { - console.log(`[inspector] Auto-restart aborted: disconnect called during reconnect`); + logger.info(`[inspector] Auto-restart aborted: disconnect called during reconnect`); } void this.disconnect(); return; @@ -688,7 +691,7 @@ export class ConnectionManager extends EventEmitter { }) .catch(() => { if (this.debug) { - console.log(`[inspector] Auto-restart failed, disconnecting`); + logger.info(`[inspector] Auto-restart failed, disconnecting`); } void this.disconnect().catch(() => { /* cleanup best-effort */ @@ -814,7 +817,7 @@ export class ConnectionManager extends EventEmitter { }; if (this.debug) { - console.log(`[inspector] Environment state updated:`, partial); + logger.info(`[inspector] Environment state updated:`, partial); } return { ...this.environmentState }; @@ -827,7 +830,7 @@ export class ConnectionManager extends EventEmitter { this.environmentState = getDefaultEnvironmentState(); if (this.debug) { - console.log(`[inspector] Environment state reset to defaults`); + logger.info(`[inspector] Environment state reset to defaults`); } return { ...this.environmentState }; @@ -852,8 +855,7 @@ export class ConnectionManager extends EventEmitter { this.widgetServer = new WidgetServer({ debug: this.debug }); await this.widgetServer.start(); if (this.debug) { - // eslint-disable-next-line no-console - console.log( + logger.info( `[inspector] Shared WidgetServer started on port ${this.widgetServer.getPort()}` ); } @@ -876,8 +878,7 @@ export class ConnectionManager extends EventEmitter { await this.widgetServer.stop(); this.widgetServer = null; if (this.debug) { - // eslint-disable-next-line no-console - console.log(`[inspector] Shared WidgetServer stopped`); + logger.info(`[inspector] Shared WidgetServer stopped`); } } } @@ -899,7 +900,7 @@ export class ConnectionManager extends EventEmitter { setAuthToken(token: string): void { this.authToken = token; if (this.debug) { - console.log(`[inspector] Auth token set`); + logger.info(`[inspector] Auth token set`); } } @@ -920,7 +921,7 @@ export class ConnectionManager extends EventEmitter { setInspectorUrl(url: string): void { this.inspectorUrl = url; if (this.debug) { - console.log(`[inspector] Inspector URL set to: ${url}`); + logger.info(`[inspector] Inspector URL set to: ${url}`); } } @@ -949,7 +950,7 @@ export class ConnectionManager extends EventEmitter { this.externalMcpHostContext = { ...(this.externalMcpHostContext ?? {}), ...globals }; if (this.debug) { - console.log(`[inspector] Stored external MCP hostContext:`, this.externalMcpHostContext); + logger.info(`[inspector] Stored external MCP hostContext:`, this.externalMcpHostContext); } // Map OpenAI globals format OR MCP hostContext format to EnvironmentState @@ -1034,7 +1035,7 @@ export class ConnectionManager extends EventEmitter { // Only update if we have changes if (Object.keys(update).length === 0) { if (this.debug) { - console.log(`[inspector] No relevant environment fields in globals, skipping update`); + logger.info(`[inspector] No relevant environment fields in globals, skipping update`); } return; } @@ -1043,7 +1044,7 @@ export class ConnectionManager extends EventEmitter { this.setEnvironmentState(update); if (this.debug) { - console.log(`[inspector] Environment updated from external globals:`, update); + logger.info(`[inspector] Environment updated from external globals:`, update); } } @@ -1071,7 +1072,7 @@ export class ConnectionManager extends EventEmitter { return null; } catch (error) { if (this.debug) { - console.warn(`[inspector] Error reading resource ${uri}:`, error); + logger.warn(`[inspector] Error reading resource ${uri}:`, error); } return null; } @@ -1099,7 +1100,7 @@ export class ConnectionManager extends EventEmitter { setOAuthProvider(provider: InspectorOAuthProvider): void { this.oauthProvider = provider; if (this.debug) { - console.log(`[inspector] OAuth provider set externally`); + logger.info(`[inspector] OAuth provider set externally`); } } @@ -1154,7 +1155,7 @@ export class ConnectionManager extends EventEmitter { this.emit("agentEvent", event); if (this.debug) { - console.log(`[inspector] Agent event recorded: ${type}`, event.id); + logger.info(`[inspector] Agent event recorded: ${type}`, event.id); } return event; @@ -1179,7 +1180,7 @@ export class ConnectionManager extends EventEmitter { this.agentEvents = []; if (this.debug) { - console.log(`[inspector] Cleared ${count} agent events`); + logger.info(`[inspector] Cleared ${count} agent events`); } return count; @@ -1236,7 +1237,7 @@ export class ConnectionManager extends EventEmitter { this.recordAgentEvent("agent-initialize", payload); if (this.debug) { - console.log( + logger.info( `[inspector] Agent initialize detected: ${clientInfo?.name ?? "unknown"}${clientInfo?.version ? ` v${clientInfo.version}` : ""}` ); } diff --git a/packages/inspector/src/dashboard/cdp-streamer.ts b/packages/inspector/src/dashboard/cdp-streamer.ts index 8118a373..8a5e6a01 100644 --- a/packages/inspector/src/dashboard/cdp-streamer.ts +++ b/packages/inspector/src/dashboard/cdp-streamer.ts @@ -10,6 +10,9 @@ */ import type { Page, CDPSession } from "playwright"; +import { createLogger } from "../debug/logger"; + +const logger = createLogger("cdp-streamer"); /** * Screencast frame data @@ -130,8 +133,7 @@ export class CDPStreamer { .send("Page.screencastFrameAck", { sessionId: event.sessionId }) .catch((err: unknown) => { if (this.debug) { - // eslint-disable-next-line no-console - console.warn(`[CDPStreamer] Frame ack failed for ${sessionId}:`, err); + logger.warn(`[CDPStreamer] Frame ack failed for ${sessionId}:`, err); } }); }); @@ -162,8 +164,7 @@ export class CDPStreamer { }); if (this.debug) { - // eslint-disable-next-line no-console - console.log(`[CDPStreamer] Started screencast for session ${sessionId}`); + logger.info(`[CDPStreamer] Started screencast for session ${sessionId}`); } } catch (error) { const err = error instanceof Error ? error : new Error(String(error)); @@ -194,8 +195,7 @@ export class CDPStreamer { } if (this.debug) { - // eslint-disable-next-line no-console - console.log( + logger.info( `[CDPStreamer] Updating dimensions for ${sessionId}: ${width}x${height} (was ${session.currentDimensions.width}x${session.currentDimensions.height})` ); } @@ -217,15 +217,13 @@ export class CDPStreamer { session.currentDimensions = { width, height }; if (this.debug) { - // eslint-disable-next-line no-console - console.log(`[CDPStreamer] Restarted screencast for ${sessionId} with new dimensions`); + logger.info(`[CDPStreamer] Restarted screencast for ${sessionId} with new dimensions`); } return true; } catch (error) { if (this.debug) { - // eslint-disable-next-line no-console - console.warn(`[CDPStreamer] Failed to update dimensions for ${sessionId}:`, error); + logger.warn(`[CDPStreamer] Failed to update dimensions for ${sessionId}:`, error); } return false; } @@ -253,8 +251,7 @@ export class CDPStreamer { await session.cdpSession.detach(); } catch (error) { if (this.debug) { - // eslint-disable-next-line no-console - console.warn(`[CDPStreamer] Error stopping screencast for ${sessionId}:`, error); + logger.warn(`[CDPStreamer] Error stopping screencast for ${sessionId}:`, error); } } @@ -262,8 +259,7 @@ export class CDPStreamer { this.sessions.delete(sessionId); if (this.debug) { - // eslint-disable-next-line no-console - console.log(`[CDPStreamer] Stopped screencast for session ${sessionId}`); + logger.info(`[CDPStreamer] Stopped screencast for session ${sessionId}`); } } diff --git a/packages/inspector/src/debug/logger.ts b/packages/inspector/src/debug/logger.ts new file mode 100644 index 00000000..d9ab25ef --- /dev/null +++ b/packages/inspector/src/debug/logger.ts @@ -0,0 +1,98 @@ +/** + * Structured logger with level gating, timestamps, and source prefixes. + * + * Log level is controlled via the `MCP_APPS_LOG_LEVEL` environment variable. + * Valid values: "debug" | "info" | "warn" | "error" | "silent" (default: "info"). + */ + +// ============================================================================= +// Types +// ============================================================================= + +export type LogLevel = "debug" | "info" | "warn" | "error" | "silent"; + +export interface Logger { + debug(...args: unknown[]): void; + info(...args: unknown[]): void; + warn(...args: unknown[]): void; + error(...args: unknown[]): void; +} + +// ============================================================================= +// Level ordering +// ============================================================================= + +const LEVEL_ORDER: Record = { + debug: 0, + info: 1, + warn: 2, + error: 3, + silent: 4, +}; + +// ============================================================================= +// Helpers +// ============================================================================= + +function resolveLevel(): LogLevel { + const env = + typeof process !== "undefined" ? (process.env.MCP_APPS_LOG_LEVEL ?? "").toLowerCase() : ""; + if (env in LEVEL_ORDER) return env as LogLevel; + return "info"; +} + +function timestamp(): string { + return new Date().toISOString(); +} + +// ============================================================================= +// Factory +// ============================================================================= + +/** + * Create a scoped logger. + * + * @param source - Descriptive name included in every log line (e.g. "connection", "proxy-tools"). + */ +export function createLogger(source: string): Logger { + const level = resolveLevel(); + + function shouldLog(msgLevel: LogLevel): boolean { + return LEVEL_ORDER[msgLevel] >= LEVEL_ORDER[level]; + } + + function formatPrefix(msgLevel: string): string { + return `${timestamp()} [${msgLevel.toUpperCase()}] [${source}]`; + } + + /* eslint-disable no-console */ + return { + debug(...args: unknown[]) { + if (shouldLog("debug")) { + console.debug(formatPrefix("debug"), ...args); + } + }, + info(...args: unknown[]) { + if (shouldLog("info")) { + console.info(formatPrefix("info"), ...args); + } + }, + warn(...args: unknown[]) { + if (shouldLog("warn")) { + console.warn(formatPrefix("warn"), ...args); + } + }, + error(...args: unknown[]) { + if (shouldLog("error")) { + console.error(formatPrefix("error"), ...args); + } + }, + }; + /* eslint-enable no-console */ +} + +// ============================================================================= +// Default instance +// ============================================================================= + +export const defaultLogger: Logger = createLogger("inspector"); diff --git a/packages/inspector/src/dual-server.ts b/packages/inspector/src/dual-server.ts index f7b51e12..33930c1d 100644 --- a/packages/inspector/src/dual-server.ts +++ b/packages/inspector/src/dual-server.ts @@ -31,6 +31,10 @@ import { handleOAuthRoutes } from "./oauth/callback-handler"; import { ServerStore, type LocalStorageMigrationPayload } from "./persistence/server-store"; import { createWellKnownProxy } from "./oauth/wellknown-proxy"; import type { WellKnownProxyContext } from "./oauth/wellknown-proxy"; +import { createLogger } from "./debug/logger"; + +const logger = createLogger("dual-server"); + import { createConnectTool, createDisconnectTool, @@ -226,8 +230,7 @@ export function createDualInspectorServer( newCm.on("schemaUpdated", (schema: TargetServerSchema) => { // Only create once per lifecycle if (appsApp !== null) { - // eslint-disable-next-line no-console - console.warn( + logger.warn( `[dual-inspector] Target already connected. Restart inspector to connect to a different target.` ); return; @@ -256,8 +259,7 @@ export function createDualInspectorServer( // Register proxy resources on the MCP server const registeredResources = registerProxyResources(mcpServer, newCm, schema.resources); - // eslint-disable-next-line no-console - console.log( + logger.info( `[dual-inspector] /apps/mcp ready with ${schema.tools.length} proxy tools and ${registeredResources.length} proxy resources` ); }); // end schemaUpdated @@ -275,8 +277,7 @@ export function createDualInspectorServer( // Debug: log all incoming requests if (options.debug) { - // eslint-disable-next-line no-console - console.log(`[inspector] Request: ${req.method} ${url}`); + logger.info(`[inspector] Request: ${req.method} ${url}`); } // Health check @@ -556,8 +557,7 @@ export function createDualInspectorServer( // Environment update endpoint (for standalone mode widgets) // Called when the widget requests display mode or other environment changes if (url === "/update-environment") { - // eslint-disable-next-line no-console - console.log(`[inspector] /update-environment request: method=${req.method}`); + logger.info(`[inspector] /update-environment request: method=${req.method}`); // Handle CORS res.setHeader("Access-Control-Allow-Origin", "*"); @@ -565,8 +565,7 @@ export function createDualInspectorServer( res.setHeader("Access-Control-Allow-Headers", "Content-Type"); if (req.method === "OPTIONS") { - // eslint-disable-next-line no-console - console.log(`[inspector] /update-environment OPTIONS preflight - sending CORS headers`); + logger.info(`[inspector] /update-environment OPTIONS preflight - sending CORS headers`); res.writeHead(204); res.end(); return; @@ -622,8 +621,7 @@ export function createDualInspectorServer( ); if (options.debug) { - // eslint-disable-next-line no-console - console.log( + logger.info( `[inspector] Environment updated from widget, session ${data.sessionId}, resized: ${updated}` ); } @@ -875,8 +873,7 @@ export function createDualInspectorServer( try { httpServer = http.createServer((req, res) => { void handleRequest(req, res).catch((error: unknown) => { - // eslint-disable-next-line no-console - console.error("[dual-inspector] Request error:", error); + logger.error("[dual-inspector] Request error:", error); if (!res.headersSent) { res.writeHead(500, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Internal server error" })); @@ -888,12 +885,9 @@ export function createDualInspectorServer( // Use 127.0.0.1 instead of localhost to match widget server origin (avoids CORS issues) getActiveConnectionManager()?.setInspectorUrl(`http://127.0.0.1:${port}`); - // eslint-disable-next-line no-console - console.log(`[dual-inspector] Started on port ${port}`); - // eslint-disable-next-line no-console - console.log(` Agent endpoint: http://localhost:${port}/agent/mcp`); - // eslint-disable-next-line no-console - console.log(` Apps endpoint: http://localhost:${port}/apps/mcp (after connect)`); + logger.info(`[dual-inspector] Started on port ${port}`); + logger.info(` Agent endpoint: http://localhost:${port}/agent/mcp`); + logger.info(` Apps endpoint: http://localhost:${port}/apps/mcp (after connect)`); resolve(); }); httpServer.on("error", (err: Error) => { @@ -920,8 +914,7 @@ export function createDualInspectorServer( reject(err); } else { httpServer = null; - // eslint-disable-next-line no-console - console.log(`[dual-inspector] Stopped`); + logger.info(`[dual-inspector] Stopped`); resolve(); } }); diff --git a/packages/inspector/src/hosts/mcp-host.ts b/packages/inspector/src/hosts/mcp-host.ts index 98041dfd..6f6f3197 100644 --- a/packages/inspector/src/hosts/mcp-host.ts +++ b/packages/inspector/src/hosts/mcp-host.ts @@ -17,6 +17,9 @@ import { type DisplayMode, type DisplayModePlatform, } from "../types/environment-types"; +import { createLogger } from "../debug/logger"; + +const logger = createLogger("mcp-host"); // Re-export shared types for backwards compatibility export type { TrackedToolCall }; @@ -107,8 +110,7 @@ export class MCPHostEmulator extends BaseHostEmulator { value: { postMessage: (message: unknown, _targetOrigin: string) => { if (debug) { - // eslint-disable-next-line no-console - console.log("[MCP Host] Received postMessage:", JSON.stringify(message, null, 2)); + logger.info("[MCP Host] Received postMessage:", JSON.stringify(message, null, 2)); } this.handlePostMessage(message, win); }, @@ -217,7 +219,7 @@ export class MCPHostEmulator extends BaseHostEmulator { // Handle ui/initialize request if (message && message.jsonrpc === '2.0' && message.method === 'ui/initialize') { - console.log('[MCP Host Emulator] Received ui/initialize, responding...'); + logger.info('[MCP Host Emulator] Received ui/initialize, responding...'); // Respond with initialization success var response = { jsonrpc: '2.0', @@ -242,11 +244,11 @@ export class MCPHostEmulator extends BaseHostEmulator { origin: '*', source: window, })); - console.log('[MCP Host Emulator] Sent ui/initialize response'); + logger.info('[MCP Host Emulator] Sent ui/initialize response'); // Then send the tool result after a longer delay to ensure widget is ready setTimeout(function() { - console.log('[MCP Host Emulator] Sending ui/notifications/tool-result...'); + logger.info('[MCP Host Emulator] Sending ui/notifications/tool-result...'); var resultMessage = { jsonrpc: '2.0', method: 'ui/notifications/tool-result', @@ -275,7 +277,7 @@ export class MCPHostEmulator extends BaseHostEmulator { emu.displayMode = requestedMode; emu.viewport = { width: sizing.width, height: sizing.height }; - console.log('[MCP Host Emulator] Display mode changed to:', requestedMode, 'sizing:', sizing); + logger.info('[MCP Host Emulator] Display mode changed to:', requestedMode, 'sizing:', sizing); // Send response var displayModeResponse = { @@ -419,8 +421,7 @@ export class MCPHostEmulator extends BaseHostEmulator { case "logging/sendMessage": // Handle logging from widget if (debug && msg.params) { - // eslint-disable-next-line no-console - console.log("[MCP Widget Log]", msg.params.level, msg.params.data); + logger.info("[MCP Widget Log]", msg.params.level, msg.params.data); } break; @@ -432,8 +433,7 @@ export class MCPHostEmulator extends BaseHostEmulator { // Validate that mode is a valid DisplayMode value if (!isValidDisplayMode(requestedMode)) { if (debug) { - // eslint-disable-next-line no-console - console.log("[MCP Host] Invalid display mode requested:", requestedMode); + logger.info("[MCP Host] Invalid display mode requested:", requestedMode); } this.sendError( window, @@ -457,8 +457,7 @@ export class MCPHostEmulator extends BaseHostEmulator { default: if (debug) { - // eslint-disable-next-line no-console - console.log("[MCP Host] Unhandled method:", msg.method); + logger.info("[MCP Host] Unhandled method:", msg.method); } } } @@ -481,8 +480,7 @@ export class MCPHostEmulator extends BaseHostEmulator { this.currentViewport = { width: sizing.width, height: sizing.height }; if (debug) { - // eslint-disable-next-line no-console - console.log("[MCP Host] Display mode changed to:", mode, "sizing:", sizing); + logger.info("[MCP Host] Display mode changed to:", mode, "sizing:", sizing); } // Send response with granted mode diff --git a/packages/inspector/src/hosts/openai-host.ts b/packages/inspector/src/hosts/openai-host.ts index 92d7f11f..835e60f6 100644 --- a/packages/inspector/src/hosts/openai-host.ts +++ b/packages/inspector/src/hosts/openai-host.ts @@ -17,6 +17,9 @@ import { getPlatformFromDeviceType, type DisplayMode, } from "../types/environment-types"; +import { createLogger } from "../debug/logger"; + +const logger = createLogger("openai-host"); // Re-export shared types for backwards compatibility export type { TrackedToolCall }; @@ -242,7 +245,7 @@ export class OpenAIHostEmulator extends BaseHostEmulator { this.recordToolCall(name, args); if (debug) { - // eslint-disable-next-line no-console - console.log("[OpenAI Host] callTool:", name, args); + logger.info("[OpenAI Host] callTool:", name, args); } return { output: JSON.stringify({ mock: true }) }; }, @@ -444,8 +444,7 @@ export class OpenAIHostEmulator extends BaseHostEmulator { // Validate responseData is a non-null object before accessing properties if (typeof responseData !== "object" || responseData === null) { - // eslint-disable-next-line no-console - console.log("[MCP Host] Tool response is not a valid object:", responseData); + logger.info("[MCP Host] Tool response is not a valid object:", responseData); return; } @@ -299,8 +300,7 @@ export async function deliverToolCallResponse(options: DeliverToolResponseOption : null; if (!toolName) { - // eslint-disable-next-line no-console - console.log("[MCP Host] Tool response missing valid name/toolName string:", responseData); + logger.info("[MCP Host] Tool response missing valid name/toolName string:", responseData); return; } @@ -309,8 +309,7 @@ export async function deliverToolCallResponse(options: DeliverToolResponseOption const pending = w.__pendingToolCalls?.[toolName]; if (!pending || pending.length === 0) { - // eslint-disable-next-line no-console - console.log("[MCP Host] No pending calls for tool:", toolName); + logger.info("[MCP Host] No pending calls for tool:", toolName); return; } @@ -329,8 +328,7 @@ export async function deliverToolCallResponse(options: DeliverToolResponseOption }, "*" ); - // eslint-disable-next-line no-console - console.log("[MCP Host] Delivered synced tool response:", toolName, call.messageId); + logger.info("[MCP Host] Delivered synced tool response:", toolName, call.messageId); } }, data); /* eslint-enable no-undef */ diff --git a/packages/inspector/src/session/session-store.ts b/packages/inspector/src/session/session-store.ts index f37b2d86..be01b6e2 100644 --- a/packages/inspector/src/session/session-store.ts +++ b/packages/inspector/src/session/session-store.ts @@ -2,6 +2,10 @@ import type { Page } from "playwright"; import type { InspectorEvent, TrackedDialog, WidgetToolCall } from "../types"; import type { DetectedProtocol } from "../ui-host"; import type { ConsoleLogEntry } from "../tools/get-console-logs"; +import { createLogger } from "../debug/logger"; + +const logger = createLogger("session-store"); + import type { ActiveWidgetSession, SessionInfo, @@ -76,7 +80,7 @@ export class SessionStore { this.sessions.set(options.sessionId, session); if (this.debug) { - console.log(`[SessionStore] Created session ${options.sessionId}`); + logger.info(`[SessionStore] Created session ${options.sessionId}`); } return session; @@ -257,7 +261,7 @@ export class SessionStore { this.sessions.delete(sessionId); if (this.debug) { - console.log(`[SessionStore] Closed session ${sessionId}`); + logger.info(`[SessionStore] Closed session ${sessionId}`); } return true; @@ -284,7 +288,7 @@ export class SessionStore { this.sessions.clear(); if (this.debug) { - console.log(`[SessionStore] Closed ${count} session(s)`); + logger.info(`[SessionStore] Closed ${count} session(s)`); } return count; @@ -313,7 +317,7 @@ export class SessionStore { await this.closeAll(); if (this.debug) { - console.log(`[SessionStore] Disposed`); + logger.info(`[SessionStore] Disposed`); } } @@ -330,7 +334,7 @@ export class SessionStore { for (const sessionId of expiredIds) { this.delete(sessionId); if (this.debug) { - console.log(`[SessionStore] Expired session ${sessionId}`); + logger.info(`[SessionStore] Expired session ${sessionId}`); } } } diff --git a/packages/inspector/src/standalone-server.ts b/packages/inspector/src/standalone-server.ts index daa266f6..b3ca0b4d 100644 --- a/packages/inspector/src/standalone-server.ts +++ b/packages/inspector/src/standalone-server.ts @@ -62,6 +62,9 @@ import { import type { InspectorEventType } from "./types"; import { handleOAuthRoutes } from "./oauth/callback-handler"; import type { InspectorOAuthProvider } from "./oauth/provider"; +import { createLogger } from "./debug/logger"; + +const logger = createLogger("standalone-server"); // ============================================================================= // VALID INSPECTOR EVENT TYPES (for validation) @@ -1011,8 +1014,7 @@ export function createStandaloneInspectorServer( ); if (options.debug) { - // eslint-disable-next-line no-console - console.log( + logger.info( `[inspector] Environment updated from widget, session ${data.sessionId}, resized: ${updated}` ); } @@ -1276,8 +1278,7 @@ export function createStandaloneInspectorServer( try { httpServer = http.createServer((req, res) => { void handleRequest(req, res).catch((error: unknown) => { - // eslint-disable-next-line no-console - console.error("[inspector] Request error:", error); + logger.error("[inspector] Request error:", error); if (!res.headersSent) { res.writeHead(500, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Internal server error" })); @@ -1322,15 +1323,13 @@ export function createStandaloneInspectorServer( // Mark server as ready now that auto-connect succeeded isReady = true; if (options.debug) { - // eslint-disable-next-line no-console - console.log(`[inspector] Auto-connected to: ${targetUrl}`); + logger.info(`[inspector] Auto-connected to: ${targetUrl}`); } resolve(); }) .catch((error: unknown) => { const message = error instanceof Error ? error.message : String(error); - // eslint-disable-next-line no-console - console.error(`[inspector] Auto-connect failed: ${message}`); + logger.error(`[inspector] Auto-connect failed: ${message}`); // Close the HTTP server since we can't proceed httpServer?.close(); reject(new Error(`Auto-connect to ${targetUrl} failed: ${message}`)); diff --git a/packages/inspector/src/tools/call-tool.ts b/packages/inspector/src/tools/call-tool.ts index 9e90cdac..e559486b 100644 --- a/packages/inspector/src/tools/call-tool.ts +++ b/packages/inspector/src/tools/call-tool.ts @@ -7,6 +7,10 @@ import { defineTool } from "@mcp-apps-kit/core"; import type { ConnectionRegistry } from "../connection-registry"; import type { CallToolOutput, ToolHints } from "../types"; import { UIHostManager } from "../ui-host"; +import { createLogger } from "../debug/logger"; + +const logger = createLogger("call-tool"); + import { findUIResourceForTool, fetchWidgetHTML, @@ -155,7 +159,7 @@ export function createCallToolTool(registry: ConnectionRegistry) { const widgetSessionId = urlMatch?.[1]; if (!widgetSessionId) { - console.warn( + logger.warn( `[call_tool] Failed to extract widgetSessionId from page URL: ${pageUrl}` ); // Widget session ID extraction failed, return result without session @@ -167,8 +171,7 @@ export function createCallToolTool(registry: ConnectionRegistry) { // Create widget session in session manager const sessionManager = connectionManager.getWidgetSessionManager(); - // eslint-disable-next-line no-console - console.log( + logger.info( `[call_tool] Creating session for ${input.name}, widgetSessionId: ${widgetSessionId}, hostUrl: ${pageUrl}` ); const session = await sessionManager.createSession( @@ -184,14 +187,13 @@ export function createCallToolTool(registry: ConnectionRegistry) { ); sessionId = session.id; - // eslint-disable-next-line no-console - console.log(`[call_tool] Session created: ${sessionId}`); + logger.info(`[call_tool] Session created: ${sessionId}`); } } } catch (error) { // Widget rendering failed, but tool call succeeded // Continue without session - console.warn(`[call_tool] Failed to render widget:`, error); + logger.warn(`[call_tool] Failed to render widget:`, error); } // Add hints when widget session is created diff --git a/packages/inspector/src/tools/widget-control.ts b/packages/inspector/src/tools/widget-control.ts index f7a3e3d4..7305de41 100644 --- a/packages/inspector/src/tools/widget-control.ts +++ b/packages/inspector/src/tools/widget-control.ts @@ -12,6 +12,10 @@ import { z } from "zod"; import { defineTool } from "@mcp-apps-kit/core"; import type { ConnectionRegistry } from "../connection-registry"; +import { createLogger } from "../debug/logger"; + +const logger = createLogger("widget-control"); + import type { WidgetEvaluateOutput, WidgetClickOutput, @@ -1008,7 +1012,7 @@ export function createWidgetRefreshTool(registry: ConnectionRegistry) { // Widget update failed, but tool call succeeded const updateErrorMessage = updateError instanceof Error ? updateError.message : String(updateError); - console.warn( + logger.warn( `[widget-refresh] Failed to push update to widget (session: ${input.sessionId}, protocol: ${session.protocol}):`, updateErrorMessage ); diff --git a/packages/inspector/src/widget-server.ts b/packages/inspector/src/widget-server.ts index 7b8cd12b..e8a0945e 100644 --- a/packages/inspector/src/widget-server.ts +++ b/packages/inspector/src/widget-server.ts @@ -23,6 +23,10 @@ import { createServer, type Server, type IncomingMessage, type ServerResponse } from "node:http"; import { randomUUID } from "node:crypto"; import type { EnvironmentState } from "./types"; +import { createLogger } from "./debug/logger"; + +const logger = createLogger("widget-server"); + import { generateMcpHostPage as generateMcpHostPageTemplate, generateOpenAIHostPage as generateOpenAIHostPageTemplate, @@ -427,8 +431,7 @@ export class WidgetServer { */ private log(message: string): void { if (this.options.debug) { - // eslint-disable-next-line no-console - console.log(`[WidgetServer] ${message}`); + logger.info(`[WidgetServer] ${message}`); } } diff --git a/packages/inspector/src/widget-session-manager.ts b/packages/inspector/src/widget-session-manager.ts index 9023107a..b6a40cb4 100644 --- a/packages/inspector/src/widget-session-manager.ts +++ b/packages/inspector/src/widget-session-manager.ts @@ -56,6 +56,9 @@ import { // Import from session module import { SessionStore, setupPageListeners, deliverToolCallResponse } from "./session"; import type { ActiveWidgetSession, SessionInfo, SessionSource, ProxyMetadata } from "./session"; +import { createLogger } from "./debug/logger"; + +const logger = createLogger("widget-session-manager"); // Re-export types for backwards compatibility export type { ActiveWidgetSession, SessionInfo, SessionSource, ProxyMetadata }; @@ -119,7 +122,7 @@ export class WidgetSessionManager extends EventEmitter { const session = this.store.peek(sessionId); if (!session) { if (this.debug) { - console.log( + logger.info( `[WidgetSessionManager] Dropping event ${type} - session ${sessionId} not found` ); } @@ -141,7 +144,7 @@ export class WidgetSessionManager extends EventEmitter { this.emit("event", event); if (this.debug) { - console.log(`[WidgetSessionManager] Recorded event ${type} for session ${sessionId}`); + logger.info(`[WidgetSessionManager] Recorded event ${type} for session ${sessionId}`); } return event; @@ -231,7 +234,7 @@ export class WidgetSessionManager extends EventEmitter { this.recordEvent(sessionId, "tool-result", { toolName, result: toolResult }, "host", protocol); if (this.debug) { - console.log(`[WidgetSessionManager] Created session ${sessionId} for tool ${toolName}`); + logger.info(`[WidgetSessionManager] Created session ${sessionId} for tool ${toolName}`); } return session; @@ -277,7 +280,7 @@ export class WidgetSessionManager extends EventEmitter { const session = this.store.peek(sessionId); if (!session) { if (this.debug) { - console.log(`[WidgetSessionManager] Session not found: ${sessionId}`); + logger.info(`[WidgetSessionManager] Session not found: ${sessionId}`); } return false; } @@ -312,7 +315,7 @@ export class WidgetSessionManager extends EventEmitter { ); if (this.debug) { - console.log(`[WidgetSessionManager] Recorded tool call ${toolName} for session ${sessionId}`); + logger.info(`[WidgetSessionManager] Recorded tool call ${toolName} for session ${sessionId}`); } return true; @@ -370,7 +373,7 @@ export class WidgetSessionManager extends EventEmitter { await page.setViewportSize(viewport); if (this.debug) { - console.log( + logger.info( `[WidgetSessionManager] Resized page viewport to ${viewport.width}x${viewport.height}` ); } @@ -406,8 +409,7 @@ export class WidgetSessionManager extends EventEmitter { params: { hostContext: ctx }, }; iframe.contentWindow.postMessage(message, "*"); - // eslint-disable-next-line no-console - console.log("[MCP Host] Sent ui/notifications/host-context-changed", ctx); + logger.info("[MCP Host] Sent ui/notifications/host-context-changed", ctx); } }, hostContext); /* eslint-enable no-undef */ @@ -437,8 +439,7 @@ export class WidgetSessionManager extends EventEmitter { const iframe = document.getElementById("widget-frame") as HTMLIFrameElement | null; if (iframe?.contentWindow) { iframe.contentWindow.postMessage(message, "*"); - // eslint-disable-next-line no-console - console.log("[OpenAI Host] Sent globals sync:", message.data); + logger.info("[OpenAI Host] Sent globals sync:", message.data); } }, syncMessage); /* eslint-enable no-undef */ @@ -451,13 +452,13 @@ export class WidgetSessionManager extends EventEmitter { this.store.touch(sessionId); if (this.debug) { - console.log(`[WidgetSessionManager] Updated globals for session ${sessionId}`); + logger.info(`[WidgetSessionManager] Updated globals for session ${sessionId}`); } return true; } catch (error) { if (this.debug) { - console.warn( + logger.warn( `[WidgetSessionManager] Error updating globals for session ${sessionId}:`, error ); @@ -603,14 +604,14 @@ export class WidgetSessionManager extends EventEmitter { if (newViewport) { await session.page.setViewportSize(newViewport); if (this.debug) { - console.log( + logger.info( `[WidgetSessionManager] Resized viewport for session ${session.id} to ${newViewport.width}x${newViewport.height} (displayMode: ${displayMode ?? "unchanged"})` ); } } } catch (error) { if (this.debug) { - console.warn( + logger.warn( `[WidgetSessionManager] Failed to resize viewport for session ${session.id}:`, error ); @@ -640,13 +641,13 @@ export class WidgetSessionManager extends EventEmitter { } if (this.debug) { - console.log( + logger.info( `[WidgetSessionManager] Delivered ${type} event to session ${session.id} (${protocol})` ); } } catch (error) { if (this.debug) { - console.warn( + logger.warn( `[WidgetSessionManager] Error delivering ${type} event to session ${session.id}:`, error ); @@ -743,8 +744,7 @@ export class WidgetSessionManager extends EventEmitter { if (storeOnHost) { const w = window as Window & { __mcpHostContextUpdates?: Record }; w.__mcpHostContextUpdates = { ...(w.__mcpHostContextUpdates ?? {}), ...(p as object) }; - // eslint-disable-next-line no-console - console.log("[MCP Host] Stored hostContext update for ui/initialize:", p); + logger.info("[MCP Host] Stored hostContext update for ui/initialize:", p); } const iframe = document.getElementById("widget-frame") as HTMLIFrameElement | null; @@ -757,8 +757,7 @@ export class WidgetSessionManager extends EventEmitter { }, "*" ); - // eslint-disable-next-line no-console - console.log("[MCP Host] Sent synced event:", m, p); + logger.info("[MCP Host] Sent synced event:", m, p); } }, { method, params, storeOnHost: isHostContextUpdate } @@ -794,8 +793,7 @@ export class WidgetSessionManager extends EventEmitter { const iframe = document.getElementById("widget-frame") as HTMLIFrameElement | null; if (iframe?.contentWindow) { iframe.contentWindow.postMessage(message, "*"); - // eslint-disable-next-line no-console - console.log("[OpenAI Host] Sent synced event:", message.syncType, message.data); + logger.info("[OpenAI Host] Sent synced event:", message.syncType, message.data); } }, syncMessage); /* eslint-enable no-undef */ @@ -827,7 +825,7 @@ export class WidgetSessionManager extends EventEmitter { const frame = session.page.frame({ name: "widget-frame" }); if (!frame) { if (this.debug) { - console.log(`[WidgetSessionManager] No widget-frame found for session ${session.id}`); + logger.info(`[WidgetSessionManager] No widget-frame found for session ${session.id}`); } continue; } @@ -836,11 +834,11 @@ export class WidgetSessionManager extends EventEmitter { await this.applyDomEventToFrame(frame, type, data); this.store.touch(session.id); if (this.debug) { - console.log(`[WidgetSessionManager] Applied ${type} to session ${session.id}`); + logger.info(`[WidgetSessionManager] Applied ${type} to session ${session.id}`); } } catch (error) { if (this.debug) { - console.warn(`[WidgetSessionManager] Failed to apply ${type}:`, error); + logger.warn(`[WidgetSessionManager] Failed to apply ${type}:`, error); } } } @@ -1009,7 +1007,7 @@ export class WidgetSessionManager extends EventEmitter { const result = await this.store.close(sessionId); if (this.debug && result) { - console.log(`[WidgetSessionManager] Closed session ${sessionId}`); + logger.info(`[WidgetSessionManager] Closed session ${sessionId}`); } return result; @@ -1033,7 +1031,7 @@ export class WidgetSessionManager extends EventEmitter { const count = await this.store.closeAll(); if (this.debug) { - console.log(`[WidgetSessionManager] Closed ${count} session(s)`); + logger.info(`[WidgetSessionManager] Closed ${count} session(s)`); } return count; @@ -1046,7 +1044,7 @@ export class WidgetSessionManager extends EventEmitter { await this.store.dispose(); if (this.debug) { - console.log(`[WidgetSessionManager] Disposed`); + logger.info(`[WidgetSessionManager] Disposed`); } } diff --git a/packages/inspector/tests/logger.test.ts b/packages/inspector/tests/logger.test.ts new file mode 100644 index 00000000..dbfd8282 --- /dev/null +++ b/packages/inspector/tests/logger.test.ts @@ -0,0 +1,108 @@ +/** + * Logger module tests + */ + +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { createLogger, defaultLogger } from "../src/debug/logger"; +import type { LogLevel } from "../src/debug/logger"; + +describe("createLogger", () => { + const originalEnv = process.env.MCP_APPS_LOG_LEVEL; + + beforeEach(() => { + vi.spyOn(console, "debug").mockImplementation(() => {}); + vi.spyOn(console, "info").mockImplementation(() => {}); + vi.spyOn(console, "warn").mockImplementation(() => {}); + vi.spyOn(console, "error").mockImplementation(() => {}); + }); + + afterEach(() => { + vi.restoreAllMocks(); + if (originalEnv === undefined) { + delete process.env.MCP_APPS_LOG_LEVEL; + } else { + process.env.MCP_APPS_LOG_LEVEL = originalEnv; + } + }); + + it("exports defaultLogger instance", () => { + expect(defaultLogger).toBeDefined(); + expect(typeof defaultLogger.info).toBe("function"); + expect(typeof defaultLogger.debug).toBe("function"); + expect(typeof defaultLogger.warn).toBe("function"); + expect(typeof defaultLogger.error).toBe("function"); + }); + + it("includes timestamp and source prefix in output", () => { + const logger = createLogger("test-source"); + logger.info("hello"); + + expect(console.info).toHaveBeenCalledOnce(); + const prefix = (console.info as ReturnType).mock.calls[0][0] as string; + // Timestamp: ISO format + expect(prefix).toMatch(/^\d{4}-\d{2}-\d{2}T/); + // Level + expect(prefix).toContain("[INFO]"); + // Source + expect(prefix).toContain("[test-source]"); + }); + + it("default level is info — suppresses debug", () => { + delete process.env.MCP_APPS_LOG_LEVEL; + const logger = createLogger("s"); + logger.debug("hidden"); + logger.info("shown"); + expect(console.debug).not.toHaveBeenCalled(); + expect(console.info).toHaveBeenCalledOnce(); + }); + + it("respects MCP_APPS_LOG_LEVEL=debug", () => { + process.env.MCP_APPS_LOG_LEVEL = "debug"; + const logger = createLogger("s"); + logger.debug("visible"); + expect(console.debug).toHaveBeenCalledOnce(); + }); + + it("respects MCP_APPS_LOG_LEVEL=error — suppresses info and warn", () => { + process.env.MCP_APPS_LOG_LEVEL = "error"; + const logger = createLogger("s"); + logger.info("no"); + logger.warn("no"); + logger.error("yes"); + expect(console.info).not.toHaveBeenCalled(); + expect(console.warn).not.toHaveBeenCalled(); + expect(console.error).toHaveBeenCalledOnce(); + }); + + it("silent level suppresses everything", () => { + process.env.MCP_APPS_LOG_LEVEL = "silent"; + const logger = createLogger("s"); + logger.debug("no"); + logger.info("no"); + logger.warn("no"); + logger.error("no"); + expect(console.debug).not.toHaveBeenCalled(); + expect(console.info).not.toHaveBeenCalled(); + expect(console.warn).not.toHaveBeenCalled(); + expect(console.error).not.toHaveBeenCalled(); + }); + + it("falls back to info for invalid MCP_APPS_LOG_LEVEL", () => { + process.env.MCP_APPS_LOG_LEVEL = "GARBAGE"; + const logger = createLogger("s"); + logger.debug("hidden"); + logger.info("shown"); + expect(console.debug).not.toHaveBeenCalled(); + expect(console.info).toHaveBeenCalledOnce(); + }); + + it("passes additional arguments through", () => { + const logger = createLogger("s"); + const obj = { foo: 1 }; + logger.warn("msg", obj, 42); + const args = (console.warn as ReturnType).mock.calls[0]; + expect(args[1]).toBe("msg"); + expect(args[2]).toBe(obj); + expect(args[3]).toBe(42); + }); +}); diff --git a/packages/inspector/tests/widget-session-manager.test.ts b/packages/inspector/tests/widget-session-manager.test.ts index 49ddf82d..8b308cc4 100644 --- a/packages/inspector/tests/widget-session-manager.test.ts +++ b/packages/inspector/tests/widget-session-manager.test.ts @@ -229,7 +229,7 @@ describe("WidgetSessionManager", () => { }); it("should log creation when debug is enabled", async () => { - const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + const consoleSpy = vi.spyOn(console, "info").mockImplementation(() => {}); const debugManager = new WidgetSessionManager({ debug: true }); const mockPage = createMockPage(); @@ -243,6 +243,7 @@ describe("WidgetSessionManager", () => { ); expect(consoleSpy).toHaveBeenCalledWith( + expect.stringContaining("[widget-session-manager]"), expect.stringContaining("[WidgetSessionManager] Created session") ); @@ -907,12 +908,15 @@ describe("WidgetSessionManager", () => { }); it("should log disposal when debug is enabled", async () => { - const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + const consoleSpy = vi.spyOn(console, "info").mockImplementation(() => {}); const debugManager = new WidgetSessionManager({ debug: true }); await debugManager.dispose(); - expect(consoleSpy).toHaveBeenCalledWith("[WidgetSessionManager] Disposed"); + expect(consoleSpy).toHaveBeenCalledWith( + expect.stringContaining("[widget-session-manager]"), + "[WidgetSessionManager] Disposed" + ); consoleSpy.mockRestore(); }); }); diff --git a/packages/testing/src/eval/mcp/evaluator.ts b/packages/testing/src/eval/mcp/evaluator.ts index cc199b29..41b0ecc4 100644 --- a/packages/testing/src/eval/mcp/evaluator.ts +++ b/packages/testing/src/eval/mcp/evaluator.ts @@ -34,6 +34,22 @@ import { wrapWithErrorInjection, type ToolErrorConfig } from "./error-injection" import { runBatch, type BatchEvalCase, type BatchOptions, type BatchResult } from "./batch"; import type { TestClient, TestServer } from "../../types"; +// Lightweight scoped logger (mirrors inspector's createLogger, avoids cross-package dep) +const evaluatorLogger = (() => { + const source = "testing:evaluator"; + const ts = () => new Date().toISOString(); + const LEVELS: Record = { debug: 0, info: 1, warn: 2, error: 3, silent: 4 }; + const envLevel = (process.env.MCP_APPS_LOG_LEVEL ?? "").toLowerCase(); + const threshold: number = LEVELS[envLevel] ?? 1; // 1 = info + const ok = (l: string): boolean => (LEVELS[l] ?? 1) >= threshold; + return { + // eslint-disable-next-line no-console + warn: (...args: unknown[]) => { + if (ok("warn")) console.warn(`${ts()} [WARN] [${source}]`, ...args); + }, + }; +})(); + // Type for App from @mcp-apps-kit/core (avoiding direct dependency) interface App { start(options?: { port?: number; transport?: string }): Promise; @@ -771,7 +787,7 @@ export const describeEval: DescribeWithSkip = Object.assign( | undefined; if (!globalDescribe) { - console.warn("[describeEval] No test framework detected. Skipping:", name); + evaluatorLogger.warn("No test framework detected. Skipping:", name); return; } diff --git a/packages/ui-react/src/hooks.ts b/packages/ui-react/src/hooks.ts index 725038c6..bbf32dd0 100644 --- a/packages/ui-react/src/hooks.ts +++ b/packages/ui-react/src/hooks.ts @@ -19,6 +19,26 @@ import type { import { clientDebugLogger, type ClientDebugLogger } from "@mcp-apps-kit/ui"; import { useAppsContext } from "./context"; +// Lightweight scoped logger (mirrors inspector's createLogger, avoids cross-package dep) +const uiReactLogger = (() => { + const source = "ui-react:hooks"; + const ts = () => new Date().toISOString(); + const LEVELS: Record = { debug: 0, info: 1, warn: 2, error: 3, silent: 4 }; + // eslint-disable-next-line no-undef, @typescript-eslint/no-unnecessary-condition + const envLevel = + typeof process !== "undefined" + ? ((process.env?.MCP_APPS_LOG_LEVEL as string | undefined) ?? "").toLowerCase() + : ""; + const threshold: number = LEVELS[envLevel] ?? 1; // 1 = info + const ok = (l: string): boolean => (LEVELS[l] ?? 1) >= threshold; + return { + // eslint-disable-next-line no-console + warn: (...args: unknown[]) => { + if (ok("warn")) console.warn(`${ts()} [WARN] [${source}]`, ...args); + }, + }; +})(); + // ============================================================================= // SHARED UTILITIES // ============================================================================= @@ -297,8 +317,7 @@ export function useUpdateModelContext(): (params: UpdateModelContextParams) => P return useCallback( async (params: UpdateModelContextParams) => { if (!client) { - // eslint-disable-next-line no-console - console.warn("[useUpdateModelContext] Client not available"); + uiReactLogger.warn("Client not available"); return; } await client.updateModelContext(params);