From ff92a385bed2710da117e5e157b66a6028c3065b Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Thu, 18 Dec 2025 18:15:17 +0200 Subject: [PATCH 1/4] fix: match ui/message return type to be ContentBlock[] --- .../src/adapters/mcp-apps/adapter-runtime.ts | 509 ++++++++++-------- 1 file changed, 274 insertions(+), 235 deletions(-) diff --git a/sdks/typescript/server/src/adapters/mcp-apps/adapter-runtime.ts b/sdks/typescript/server/src/adapters/mcp-apps/adapter-runtime.ts index 7b1614a7..0dcdf51e 100644 --- a/sdks/typescript/server/src/adapters/mcp-apps/adapter-runtime.ts +++ b/sdks/typescript/server/src/adapters/mcp-apps/adapter-runtime.ts @@ -3,21 +3,18 @@ * * This module enables existing MCP-UI apps to run in new MCP Apps SEP environments * by intercepting MCP-UI protocol messages and translating them to JSON-RPC over postMessage. - * + * * Note: This file is bundled as a standalone script injected into HTML. * Types are imported from @modelcontextprotocol/ext-apps for compile-time safety only. * All runtime values (like LATEST_PROTOCOL_VERSION) must be defined locally to avoid * bundling the entire ext-apps package into the output. - * + * * @see https://github.com/modelcontextprotocol/ext-apps */ // Import types from ext-apps for compile-time type checking only // These are erased during compilation and don't affect the bundled output -import type { - McpUiHostContext, - McpUiInitializeResult, -} from '@modelcontextprotocol/ext-apps'; +import type { McpUiHostContext, McpUiInitializeResult } from '@modelcontextprotocol/ext-apps'; // ============================================================================ // Protocol Constants (must match @modelcontextprotocol/ext-apps) @@ -42,7 +39,7 @@ const LATEST_PROTOCOL_VERSION = '2025-11-21'; * - McpUiHostContextChangedNotification: "ui/notifications/host-context-changed" * - McpUiSizeChangedNotification: "ui/notifications/size-changed" * - McpUiResourceTeardownRequest: "ui/resource-teardown" - * + * * @see https://github.com/modelcontextprotocol/ext-apps/blob/main/src/spec.types.ts */ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- Used in switch cases below @@ -50,18 +47,18 @@ const METHODS = { // Lifecycle INITIALIZE: 'ui/initialize', INITIALIZED: 'ui/notifications/initialized', - + // Tool data (Host -> Guest) TOOL_INPUT: 'ui/notifications/tool-input', TOOL_INPUT_PARTIAL: 'ui/notifications/tool-input-partial', TOOL_RESULT: 'ui/notifications/tool-result', TOOL_CANCELLED: 'ui/notifications/tool-cancelled', - + // Context & UI HOST_CONTEXT_CHANGED: 'ui/notifications/host-context-changed', SIZE_CHANGED: 'ui/notifications/size-changed', RESOURCE_TEARDOWN: 'ui/resource-teardown', - + // Standard MCP methods TOOLS_CALL: 'tools/call', NOTIFICATIONS_MESSAGE: 'notifications/message', @@ -113,7 +110,7 @@ class McpAppsAdapter { private hostCapabilities: McpUiInitializeResult['hostCapabilities'] | null = null; private hostContext: McpUiHostContext | null = null; private initialized = false; - + // Current render data state (similar to window.openai in Apps SDK) private currentRenderData: { toolInput?: Record; @@ -134,14 +131,19 @@ class McpAppsAdapter { install(): boolean { this.parentWindow = window.parent; - + // Debug: Log parent window detection this.config.logger.log('[MCP Apps Adapter] Checking parent window...'); this.config.logger.log('[MCP Apps Adapter] window.parent exists:', !!this.parentWindow); - this.config.logger.log('[MCP Apps Adapter] window.parent === window:', this.parentWindow === window); - + this.config.logger.log( + '[MCP Apps Adapter] window.parent === window:', + this.parentWindow === window, + ); + if (!this.parentWindow || this.parentWindow === window) { - this.config.logger.warn('[MCP Apps Adapter] No parent window detected. Adapter will not activate.'); + this.config.logger.warn( + '[MCP Apps Adapter] No parent window detected. Adapter will not activate.', + ); return false; } @@ -169,7 +171,7 @@ class McpAppsAdapter { */ private async performInitialization(): Promise { const jsonRpcId = this.generateJsonRpcId(); - + // Create a promise to wait for the initialization response const initPromise = new Promise((resolve, reject) => { this.pendingRequests.set(String(jsonRpcId), { @@ -181,26 +183,31 @@ class McpAppsAdapter { this.hostCapabilities = res?.hostCapabilities ?? null; this.hostContext = res?.hostContext ?? null; this.initialized = true; - + // Send initialized notification this.sendJsonRpcNotification(METHODS.INITIALIZED, {}); - + // Update current render data with host context (using McpUiHostContext type) if (this.hostContext) { if (this.hostContext.theme) this.currentRenderData.theme = this.hostContext.theme; - if (this.hostContext.displayMode) this.currentRenderData.displayMode = this.hostContext.displayMode as 'inline' | 'pip' | 'fullscreen'; + if (this.hostContext.displayMode) + this.currentRenderData.displayMode = this.hostContext.displayMode as + | 'inline' + | 'pip' + | 'fullscreen'; if (this.hostContext.locale) this.currentRenderData.locale = this.hostContext.locale; - if (this.hostContext.viewport?.maxHeight) this.currentRenderData.maxHeight = this.hostContext.viewport.maxHeight; + if (this.hostContext.viewport?.maxHeight) + this.currentRenderData.maxHeight = this.hostContext.viewport.maxHeight; } - + // Send initial render data to MCP-UI app this.sendRenderData(); - + // Signal ready to MCP-UI app this.dispatchMessageToIframe({ type: 'ui-lifecycle-iframe-ready', }); - + resolve(); }, reject: (error: unknown) => { @@ -216,7 +223,7 @@ class McpAppsAdapter { }); // Resolve the promise to allow the adapter to proceed resolve(); - }, this.config.timeout) + }, this.config.timeout), }); }); @@ -225,10 +232,10 @@ class McpAppsAdapter { this.sendJsonRpcRequest(jsonRpcId, METHODS.INITIALIZE, { appInfo: { name: 'mcp-ui-adapter', - version: '1.0.0' + version: '1.0.0', }, appCapabilities: {}, - protocolVersion: LATEST_PROTOCOL_VERSION + protocolVersion: LATEST_PROTOCOL_VERSION, }); this.config.logger.log('[MCP Apps Adapter] ui/initialize request sent'); @@ -254,7 +261,10 @@ class McpAppsAdapter { this.parentWindow.postMessage = this.originalPostMessage; this.config.logger.log('[MCP Apps Adapter] Restored original parent.postMessage'); } catch (error) { - this.config.logger.error('[MCP Apps Adapter] Failed to restore original postMessage:', error); + this.config.logger.error( + '[MCP Apps Adapter] Failed to restore original postMessage:', + error, + ); } } @@ -270,7 +280,7 @@ class McpAppsAdapter { const postMessageInterceptor: ParentPostMessage = ( message: unknown, targetOriginOrOptions?: string | WindowPostMessageOptions, - transfer?: Transferable[] + transfer?: Transferable[], ): void => { if (this.isMCPUIMessage(message)) { const mcpMessage = message as MCPUIMessage; @@ -294,7 +304,10 @@ class McpAppsAdapter { this.parentWindow.postMessage = postMessageInterceptor; } } catch (error) { - this.config.logger.error('[MCP Apps Adapter] Failed to monkey-patch parent.postMessage:', error); + this.config.logger.error( + '[MCP Apps Adapter] Failed to monkey-patch parent.postMessage:', + error, + ); } } @@ -303,14 +316,16 @@ class McpAppsAdapter { return false; } const msg = message as Record; - return typeof msg.type === 'string' && - (msg.type.startsWith('ui-') || - ['tool', 'prompt', 'intent', 'notify', 'link'].includes(msg.type)); + return ( + typeof msg.type === 'string' && + (msg.type.startsWith('ui-') || + ['tool', 'prompt', 'intent', 'notify', 'link'].includes(msg.type)) + ); } /** * Handles messages coming from the Host (JSON-RPC) and translates them to MCP-UI messages - * + * * MCP Apps SEP protocol methods (from @modelcontextprotocol/ext-apps): * - ui/notifications/tool-input: Complete tool arguments * - ui/notifications/tool-input-partial: Streaming partial tool arguments @@ -337,57 +352,59 @@ class McpAppsAdapter { this.currentRenderData.toolInput = data.params?.arguments; this.sendRenderData(); break; - + // MCP Apps SEP: Partial/streaming tool input notification case METHODS.TOOL_INPUT_PARTIAL: // Update stored render data with partial input this.currentRenderData.toolInput = data.params?.arguments; this.sendRenderData(); break; - + // MCP Apps SEP: Tool execution result notification case METHODS.TOOL_RESULT: // Update stored render data (like Apps SDK's window.openai.toolOutput) this.currentRenderData.toolOutput = data.params; this.sendRenderData(); break; - + // MCP Apps SEP: Host context changed (theme, viewport, etc.) case METHODS.HOST_CONTEXT_CHANGED: // Update stored render data with context if (data.params?.theme) this.currentRenderData.theme = data.params.theme; - if (data.params?.displayMode) this.currentRenderData.displayMode = data.params.displayMode; + if (data.params?.displayMode) + this.currentRenderData.displayMode = data.params.displayMode; if (data.params?.locale) this.currentRenderData.locale = data.params.locale; - if (data.params?.viewport?.maxHeight) this.currentRenderData.maxHeight = data.params.viewport.maxHeight; + if (data.params?.viewport?.maxHeight) + this.currentRenderData.maxHeight = data.params.viewport.maxHeight; this.sendRenderData(); break; - + // MCP Apps SEP: Size change notification from host case METHODS.SIZE_CHANGED: // Host is informing us of size constraints if (data.params?.height) this.currentRenderData.maxHeight = data.params.height; this.sendRenderData(); break; - + // MCP Apps SEP: Tool execution was cancelled case METHODS.TOOL_CANCELLED: // Notify the widget that the tool was cancelled this.dispatchMessageToIframe({ type: 'ui-lifecycle-tool-cancelled', payload: { - reason: data.params?.reason - } + reason: data.params?.reason, + }, }); break; - + // MCP Apps SEP: Host notifies UI before teardown (this is a request, not notification) case METHODS.RESOURCE_TEARDOWN: // Notify the widget that it's about to be torn down this.dispatchMessageToIframe({ type: 'ui-lifecycle-teardown', payload: { - reason: data.params?.reason - } + reason: data.params?.reason, + }, }); // Send success response to host if (data.id) { @@ -396,34 +413,34 @@ class McpAppsAdapter { break; } } else if (data.id) { - // Handle responses to our requests - const pendingRequest = this.pendingRequests.get(String(data.id)); - if (pendingRequest) { - if (data.error) { - pendingRequest.reject(new Error(data.error.message)); - } else { - pendingRequest.resolve(data.result); - } - this.pendingRequests.delete(String(data.id)); - clearTimeout(pendingRequest.timeoutId); - - // Send response back to the app (if it was expecting one) - this.dispatchMessageToIframe({ - type: 'ui-message-response', - messageId: pendingRequest.messageId, // The original message ID from the App - payload: { - messageId: pendingRequest.messageId, - response: data.result, - error: data.error - } - }); + // Handle responses to our requests + const pendingRequest = this.pendingRequests.get(String(data.id)); + if (pendingRequest) { + if (data.error) { + pendingRequest.reject(new Error(data.error.message)); + } else { + pendingRequest.resolve(data.result); } + this.pendingRequests.delete(String(data.id)); + clearTimeout(pendingRequest.timeoutId); + + // Send response back to the app (if it was expecting one) + this.dispatchMessageToIframe({ + type: 'ui-message-response', + messageId: pendingRequest.messageId, // The original message ID from the App + payload: { + messageId: pendingRequest.messageId, + response: data.result, + error: data.error, + }, + }); + } } } /** * Handles messages coming from the App (MCP-UI) and translates them to Host (JSON-RPC) - * + * * MCP-UI message types translated to MCP Apps SEP: * - 'tool' -> tools/call request * - 'ui-size-change' -> ui/notifications/size-changed notification @@ -437,157 +454,167 @@ class McpAppsAdapter { // Acknowledge receipt immediately this.dispatchMessageToIframe({ - type: 'ui-message-received', - payload: { messageId } + type: 'ui-message-received', + payload: { messageId }, }); try { - switch (message.type) { - // MCP-UI tool call -> MCP Apps tools/call - case 'tool': { - const { toolName, params } = (message as UIActionResult).payload as { toolName: string; params: unknown }; - const jsonRpcId = this.generateJsonRpcId(); - - this.pendingRequests.set(String(jsonRpcId), { - messageId, - type: 'tool', - resolve: () => {}, // Handled in handleHostMessage - reject: () => {}, - timeoutId: setTimeout(() => { - this.pendingRequests.delete(String(jsonRpcId)); - this.dispatchMessageToIframe({ - type: 'ui-message-response', - messageId, - payload: { messageId, error: 'Timeout' } - }) - }, this.config.timeout) - }); - - this.sendJsonRpcRequest(jsonRpcId, METHODS.TOOLS_CALL, { - name: toolName, - arguments: params - }); - break; - } - - // MCP-UI size change -> MCP Apps ui/notifications/size-changed - case 'ui-size-change': { - const { width, height } = (message as MCPUIMessage & { payload: { width?: number; height?: number } }).payload; - this.sendJsonRpcNotification(METHODS.SIZE_CHANGED, { width, height }); - break; - } - - // MCP-UI notification -> MCP Apps notifications/message (logging) - case 'notify': { - const { message: msg } = (message as UIActionResult).payload as { message: string }; - this.sendJsonRpcNotification(METHODS.NOTIFICATIONS_MESSAGE, { - level: 'info', - data: msg - }); - break; - } - - // MCP-UI link -> MCP Apps ui/open-link request - case 'link': { - const { url } = (message as UIActionResult).payload as { url: string }; - const jsonRpcId = this.generateJsonRpcId(); - - this.pendingRequests.set(String(jsonRpcId), { - messageId, - type: 'link', - resolve: () => {}, - reject: () => {}, - timeoutId: setTimeout(() => { - this.pendingRequests.delete(String(jsonRpcId)); - this.dispatchMessageToIframe({ - type: 'ui-message-response', - messageId, - payload: { messageId, error: 'Timeout' } - }) - }, this.config.timeout) - }); - - this.sendJsonRpcRequest(jsonRpcId, METHODS.OPEN_LINK, { url }); - break; - } - - // MCP-UI prompt -> MCP Apps ui/message request - case 'prompt': { - const { prompt } = (message as UIActionResult).payload as { prompt: string }; - const jsonRpcId = this.generateJsonRpcId(); - - this.pendingRequests.set(String(jsonRpcId), { - messageId, - type: 'prompt', - resolve: () => {}, - reject: () => {}, - timeoutId: setTimeout(() => { - this.pendingRequests.delete(String(jsonRpcId)); - this.dispatchMessageToIframe({ - type: 'ui-message-response', - messageId, - payload: { messageId, error: 'Timeout' } - }) - }, this.config.timeout) - }); - - this.sendJsonRpcRequest(jsonRpcId, METHODS.MESSAGE, { - role: 'user', - content: { type: 'text', text: prompt } - }); - break; - } - - // MCP-UI iframe ready -> MCP Apps ui/notifications/initialized - case 'ui-lifecycle-iframe-ready': { - this.sendJsonRpcNotification(METHODS.INITIALIZED, {}); - // Also send current render data (like Apps SDK) - this.sendRenderData(); - break; - } - - // MCP-UI request render data -> Send current render data - case 'ui-request-render-data': { - this.sendRenderData(messageId); - break; - } - - // MCP-UI intent -> Currently no direct equivalent in MCP Apps - // We translate it to a ui/message with the intent description - case 'intent': { - const { intent, params } = (message as UIActionResult).payload as { intent: string; params: unknown }; - const jsonRpcId = this.generateJsonRpcId(); - - this.pendingRequests.set(String(jsonRpcId), { - messageId, - type: 'intent', - resolve: () => {}, - reject: () => {}, - timeoutId: setTimeout(() => { - this.pendingRequests.delete(String(jsonRpcId)); - this.dispatchMessageToIframe({ - type: 'ui-message-response', - messageId, - payload: { messageId, error: 'Timeout' } - }) - }, this.config.timeout) - }); - - // Translate intent to a message - this.sendJsonRpcRequest(jsonRpcId, METHODS.MESSAGE, { - role: 'user', - content: { type: 'text', text: `Intent: ${intent}. Parameters: ${JSON.stringify(params)}` } - }); - break; - } + switch (message.type) { + // MCP-UI tool call -> MCP Apps tools/call + case 'tool': { + const { toolName, params } = (message as UIActionResult).payload as { + toolName: string; + params: unknown; + }; + const jsonRpcId = this.generateJsonRpcId(); + + this.pendingRequests.set(String(jsonRpcId), { + messageId, + type: 'tool', + resolve: () => {}, // Handled in handleHostMessage + reject: () => {}, + timeoutId: setTimeout(() => { + this.pendingRequests.delete(String(jsonRpcId)); + this.dispatchMessageToIframe({ + type: 'ui-message-response', + messageId, + payload: { messageId, error: 'Timeout' }, + }); + }, this.config.timeout), + }); + + this.sendJsonRpcRequest(jsonRpcId, METHODS.TOOLS_CALL, { + name: toolName, + arguments: params, + }); + break; } - } catch (error) { - this.config.logger.error('[MCP Apps Adapter] Error handling message:', error); - this.dispatchMessageToIframe({ - type: 'ui-message-response', + + // MCP-UI size change -> MCP Apps ui/notifications/size-changed + case 'ui-size-change': { + const { width, height } = ( + message as MCPUIMessage & { payload: { width?: number; height?: number } } + ).payload; + this.sendJsonRpcNotification(METHODS.SIZE_CHANGED, { width, height }); + break; + } + + // MCP-UI notification -> MCP Apps notifications/message (logging) + case 'notify': { + const { message: msg } = (message as UIActionResult).payload as { message: string }; + this.sendJsonRpcNotification(METHODS.NOTIFICATIONS_MESSAGE, { + level: 'info', + data: msg, + }); + break; + } + + // MCP-UI link -> MCP Apps ui/open-link request + case 'link': { + const { url } = (message as UIActionResult).payload as { url: string }; + const jsonRpcId = this.generateJsonRpcId(); + + this.pendingRequests.set(String(jsonRpcId), { + messageId, + type: 'link', + resolve: () => {}, + reject: () => {}, + timeoutId: setTimeout(() => { + this.pendingRequests.delete(String(jsonRpcId)); + this.dispatchMessageToIframe({ + type: 'ui-message-response', + messageId, + payload: { messageId, error: 'Timeout' }, + }); + }, this.config.timeout), + }); + + this.sendJsonRpcRequest(jsonRpcId, METHODS.OPEN_LINK, { url }); + break; + } + + // MCP-UI prompt -> MCP Apps ui/message request + case 'prompt': { + const { prompt } = (message as UIActionResult).payload as { prompt: string }; + const jsonRpcId = this.generateJsonRpcId(); + + this.pendingRequests.set(String(jsonRpcId), { messageId, - payload: { messageId, error } - }); + type: 'prompt', + resolve: () => {}, + reject: () => {}, + timeoutId: setTimeout(() => { + this.pendingRequests.delete(String(jsonRpcId)); + this.dispatchMessageToIframe({ + type: 'ui-message-response', + messageId, + payload: { messageId, error: 'Timeout' }, + }); + }, this.config.timeout), + }); + + this.sendJsonRpcRequest(jsonRpcId, METHODS.MESSAGE, { + role: 'user', + content: [{ type: 'text', text: prompt }], + }); + break; + } + + // MCP-UI iframe ready -> MCP Apps ui/notifications/initialized + case 'ui-lifecycle-iframe-ready': { + this.sendJsonRpcNotification(METHODS.INITIALIZED, {}); + // Also send current render data (like Apps SDK) + this.sendRenderData(); + break; + } + + // MCP-UI request render data -> Send current render data + case 'ui-request-render-data': { + this.sendRenderData(messageId); + break; + } + + // MCP-UI intent -> Currently no direct equivalent in MCP Apps + // We translate it to a ui/message with the intent description + case 'intent': { + const { intent, params } = (message as UIActionResult).payload as { + intent: string; + params: unknown; + }; + const jsonRpcId = this.generateJsonRpcId(); + + this.pendingRequests.set(String(jsonRpcId), { + messageId, + type: 'intent', + resolve: () => {}, + reject: () => {}, + timeoutId: setTimeout(() => { + this.pendingRequests.delete(String(jsonRpcId)); + this.dispatchMessageToIframe({ + type: 'ui-message-response', + messageId, + payload: { messageId, error: 'Timeout' }, + }); + }, this.config.timeout), + }); + + // Translate intent to a message + this.sendJsonRpcRequest(jsonRpcId, METHODS.MESSAGE, { + role: 'user', + content: [ + { type: 'text', text: `Intent: ${intent}. Parameters: ${JSON.stringify(params)}` }, + ], + }); + break; + } + } + } catch (error) { + this.config.logger.error('[MCP Apps Adapter] Error handling message:', error); + this.dispatchMessageToIframe({ + type: 'ui-message-response', + messageId, + payload: { messageId, error }, + }); } } @@ -608,34 +635,47 @@ class McpAppsAdapter { theme: this.currentRenderData.theme, displayMode: this.currentRenderData.displayMode, maxHeight: this.currentRenderData.maxHeight, - } - } + }, + }, }); } - private sendJsonRpcRequest(id: number | string, method: string, params?: Record) { - this.originalPostMessage?.({ - jsonrpc: '2.0', - id, - method, - params - }, '*'); + private sendJsonRpcRequest( + id: number | string, + method: string, + params?: Record, + ) { + this.originalPostMessage?.( + { + jsonrpc: '2.0', + id, + method, + params, + }, + '*', + ); } private sendJsonRpcResponse(id: number | string, result: Record) { - this.originalPostMessage?.({ - jsonrpc: '2.0', - id, - result - }, '*'); + this.originalPostMessage?.( + { + jsonrpc: '2.0', + id, + result, + }, + '*', + ); } private sendJsonRpcNotification(method: string, params?: Record) { - this.originalPostMessage?.({ - jsonrpc: '2.0', - method, - params - }, '*'); + this.originalPostMessage?.( + { + jsonrpc: '2.0', + method, + params, + }, + '*', + ); } private dispatchMessageToIframe(data: MCPUIMessage): void { @@ -650,9 +690,9 @@ class McpAppsAdapter { private generateMessageId(): string { return `adapter-${Date.now()}-${++this.messageIdCounter}`; } - + private generateJsonRpcId(): number { - return ++this.messageIdCounter; + return ++this.messageIdCounter; } } @@ -675,4 +715,3 @@ function uninstallAdapter(): void { adapterInstance = null; } } - From f918f2ac59ab048eeb427685ea4e38b52b5fd55d Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Thu, 18 Dec 2025 18:16:57 +0200 Subject: [PATCH 2/4] fix: prettier --- .../src/adapters/mcp-apps/adapter-runtime.ts | 509 ++++++++---------- 1 file changed, 235 insertions(+), 274 deletions(-) diff --git a/sdks/typescript/server/src/adapters/mcp-apps/adapter-runtime.ts b/sdks/typescript/server/src/adapters/mcp-apps/adapter-runtime.ts index 0dcdf51e..32bece29 100644 --- a/sdks/typescript/server/src/adapters/mcp-apps/adapter-runtime.ts +++ b/sdks/typescript/server/src/adapters/mcp-apps/adapter-runtime.ts @@ -3,18 +3,21 @@ * * This module enables existing MCP-UI apps to run in new MCP Apps SEP environments * by intercepting MCP-UI protocol messages and translating them to JSON-RPC over postMessage. - * + * * Note: This file is bundled as a standalone script injected into HTML. * Types are imported from @modelcontextprotocol/ext-apps for compile-time safety only. * All runtime values (like LATEST_PROTOCOL_VERSION) must be defined locally to avoid * bundling the entire ext-apps package into the output. - * + * * @see https://github.com/modelcontextprotocol/ext-apps */ // Import types from ext-apps for compile-time type checking only // These are erased during compilation and don't affect the bundled output -import type { McpUiHostContext, McpUiInitializeResult } from '@modelcontextprotocol/ext-apps'; +import type { + McpUiHostContext, + McpUiInitializeResult, +} from '@modelcontextprotocol/ext-apps'; // ============================================================================ // Protocol Constants (must match @modelcontextprotocol/ext-apps) @@ -39,7 +42,7 @@ const LATEST_PROTOCOL_VERSION = '2025-11-21'; * - McpUiHostContextChangedNotification: "ui/notifications/host-context-changed" * - McpUiSizeChangedNotification: "ui/notifications/size-changed" * - McpUiResourceTeardownRequest: "ui/resource-teardown" - * + * * @see https://github.com/modelcontextprotocol/ext-apps/blob/main/src/spec.types.ts */ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- Used in switch cases below @@ -47,18 +50,18 @@ const METHODS = { // Lifecycle INITIALIZE: 'ui/initialize', INITIALIZED: 'ui/notifications/initialized', - + // Tool data (Host -> Guest) TOOL_INPUT: 'ui/notifications/tool-input', TOOL_INPUT_PARTIAL: 'ui/notifications/tool-input-partial', TOOL_RESULT: 'ui/notifications/tool-result', TOOL_CANCELLED: 'ui/notifications/tool-cancelled', - + // Context & UI HOST_CONTEXT_CHANGED: 'ui/notifications/host-context-changed', SIZE_CHANGED: 'ui/notifications/size-changed', RESOURCE_TEARDOWN: 'ui/resource-teardown', - + // Standard MCP methods TOOLS_CALL: 'tools/call', NOTIFICATIONS_MESSAGE: 'notifications/message', @@ -110,7 +113,7 @@ class McpAppsAdapter { private hostCapabilities: McpUiInitializeResult['hostCapabilities'] | null = null; private hostContext: McpUiHostContext | null = null; private initialized = false; - + // Current render data state (similar to window.openai in Apps SDK) private currentRenderData: { toolInput?: Record; @@ -131,19 +134,14 @@ class McpAppsAdapter { install(): boolean { this.parentWindow = window.parent; - + // Debug: Log parent window detection this.config.logger.log('[MCP Apps Adapter] Checking parent window...'); this.config.logger.log('[MCP Apps Adapter] window.parent exists:', !!this.parentWindow); - this.config.logger.log( - '[MCP Apps Adapter] window.parent === window:', - this.parentWindow === window, - ); - + this.config.logger.log('[MCP Apps Adapter] window.parent === window:', this.parentWindow === window); + if (!this.parentWindow || this.parentWindow === window) { - this.config.logger.warn( - '[MCP Apps Adapter] No parent window detected. Adapter will not activate.', - ); + this.config.logger.warn('[MCP Apps Adapter] No parent window detected. Adapter will not activate.'); return false; } @@ -171,7 +169,7 @@ class McpAppsAdapter { */ private async performInitialization(): Promise { const jsonRpcId = this.generateJsonRpcId(); - + // Create a promise to wait for the initialization response const initPromise = new Promise((resolve, reject) => { this.pendingRequests.set(String(jsonRpcId), { @@ -183,31 +181,26 @@ class McpAppsAdapter { this.hostCapabilities = res?.hostCapabilities ?? null; this.hostContext = res?.hostContext ?? null; this.initialized = true; - + // Send initialized notification this.sendJsonRpcNotification(METHODS.INITIALIZED, {}); - + // Update current render data with host context (using McpUiHostContext type) if (this.hostContext) { if (this.hostContext.theme) this.currentRenderData.theme = this.hostContext.theme; - if (this.hostContext.displayMode) - this.currentRenderData.displayMode = this.hostContext.displayMode as - | 'inline' - | 'pip' - | 'fullscreen'; + if (this.hostContext.displayMode) this.currentRenderData.displayMode = this.hostContext.displayMode as 'inline' | 'pip' | 'fullscreen'; if (this.hostContext.locale) this.currentRenderData.locale = this.hostContext.locale; - if (this.hostContext.viewport?.maxHeight) - this.currentRenderData.maxHeight = this.hostContext.viewport.maxHeight; + if (this.hostContext.viewport?.maxHeight) this.currentRenderData.maxHeight = this.hostContext.viewport.maxHeight; } - + // Send initial render data to MCP-UI app this.sendRenderData(); - + // Signal ready to MCP-UI app this.dispatchMessageToIframe({ type: 'ui-lifecycle-iframe-ready', }); - + resolve(); }, reject: (error: unknown) => { @@ -223,7 +216,7 @@ class McpAppsAdapter { }); // Resolve the promise to allow the adapter to proceed resolve(); - }, this.config.timeout), + }, this.config.timeout) }); }); @@ -232,10 +225,10 @@ class McpAppsAdapter { this.sendJsonRpcRequest(jsonRpcId, METHODS.INITIALIZE, { appInfo: { name: 'mcp-ui-adapter', - version: '1.0.0', + version: '1.0.0' }, appCapabilities: {}, - protocolVersion: LATEST_PROTOCOL_VERSION, + protocolVersion: LATEST_PROTOCOL_VERSION }); this.config.logger.log('[MCP Apps Adapter] ui/initialize request sent'); @@ -261,10 +254,7 @@ class McpAppsAdapter { this.parentWindow.postMessage = this.originalPostMessage; this.config.logger.log('[MCP Apps Adapter] Restored original parent.postMessage'); } catch (error) { - this.config.logger.error( - '[MCP Apps Adapter] Failed to restore original postMessage:', - error, - ); + this.config.logger.error('[MCP Apps Adapter] Failed to restore original postMessage:', error); } } @@ -280,7 +270,7 @@ class McpAppsAdapter { const postMessageInterceptor: ParentPostMessage = ( message: unknown, targetOriginOrOptions?: string | WindowPostMessageOptions, - transfer?: Transferable[], + transfer?: Transferable[] ): void => { if (this.isMCPUIMessage(message)) { const mcpMessage = message as MCPUIMessage; @@ -304,10 +294,7 @@ class McpAppsAdapter { this.parentWindow.postMessage = postMessageInterceptor; } } catch (error) { - this.config.logger.error( - '[MCP Apps Adapter] Failed to monkey-patch parent.postMessage:', - error, - ); + this.config.logger.error('[MCP Apps Adapter] Failed to monkey-patch parent.postMessage:', error); } } @@ -316,16 +303,14 @@ class McpAppsAdapter { return false; } const msg = message as Record; - return ( - typeof msg.type === 'string' && - (msg.type.startsWith('ui-') || - ['tool', 'prompt', 'intent', 'notify', 'link'].includes(msg.type)) - ); + return typeof msg.type === 'string' && + (msg.type.startsWith('ui-') || + ['tool', 'prompt', 'intent', 'notify', 'link'].includes(msg.type)); } /** * Handles messages coming from the Host (JSON-RPC) and translates them to MCP-UI messages - * + * * MCP Apps SEP protocol methods (from @modelcontextprotocol/ext-apps): * - ui/notifications/tool-input: Complete tool arguments * - ui/notifications/tool-input-partial: Streaming partial tool arguments @@ -352,59 +337,57 @@ class McpAppsAdapter { this.currentRenderData.toolInput = data.params?.arguments; this.sendRenderData(); break; - + // MCP Apps SEP: Partial/streaming tool input notification case METHODS.TOOL_INPUT_PARTIAL: // Update stored render data with partial input this.currentRenderData.toolInput = data.params?.arguments; this.sendRenderData(); break; - + // MCP Apps SEP: Tool execution result notification case METHODS.TOOL_RESULT: // Update stored render data (like Apps SDK's window.openai.toolOutput) this.currentRenderData.toolOutput = data.params; this.sendRenderData(); break; - + // MCP Apps SEP: Host context changed (theme, viewport, etc.) case METHODS.HOST_CONTEXT_CHANGED: // Update stored render data with context if (data.params?.theme) this.currentRenderData.theme = data.params.theme; - if (data.params?.displayMode) - this.currentRenderData.displayMode = data.params.displayMode; + if (data.params?.displayMode) this.currentRenderData.displayMode = data.params.displayMode; if (data.params?.locale) this.currentRenderData.locale = data.params.locale; - if (data.params?.viewport?.maxHeight) - this.currentRenderData.maxHeight = data.params.viewport.maxHeight; + if (data.params?.viewport?.maxHeight) this.currentRenderData.maxHeight = data.params.viewport.maxHeight; this.sendRenderData(); break; - + // MCP Apps SEP: Size change notification from host case METHODS.SIZE_CHANGED: // Host is informing us of size constraints if (data.params?.height) this.currentRenderData.maxHeight = data.params.height; this.sendRenderData(); break; - + // MCP Apps SEP: Tool execution was cancelled case METHODS.TOOL_CANCELLED: // Notify the widget that the tool was cancelled this.dispatchMessageToIframe({ type: 'ui-lifecycle-tool-cancelled', payload: { - reason: data.params?.reason, - }, + reason: data.params?.reason + } }); break; - + // MCP Apps SEP: Host notifies UI before teardown (this is a request, not notification) case METHODS.RESOURCE_TEARDOWN: // Notify the widget that it's about to be torn down this.dispatchMessageToIframe({ type: 'ui-lifecycle-teardown', payload: { - reason: data.params?.reason, - }, + reason: data.params?.reason + } }); // Send success response to host if (data.id) { @@ -413,34 +396,34 @@ class McpAppsAdapter { break; } } else if (data.id) { - // Handle responses to our requests - const pendingRequest = this.pendingRequests.get(String(data.id)); - if (pendingRequest) { - if (data.error) { - pendingRequest.reject(new Error(data.error.message)); - } else { - pendingRequest.resolve(data.result); + // Handle responses to our requests + const pendingRequest = this.pendingRequests.get(String(data.id)); + if (pendingRequest) { + if (data.error) { + pendingRequest.reject(new Error(data.error.message)); + } else { + pendingRequest.resolve(data.result); + } + this.pendingRequests.delete(String(data.id)); + clearTimeout(pendingRequest.timeoutId); + + // Send response back to the app (if it was expecting one) + this.dispatchMessageToIframe({ + type: 'ui-message-response', + messageId: pendingRequest.messageId, // The original message ID from the App + payload: { + messageId: pendingRequest.messageId, + response: data.result, + error: data.error + } + }); } - this.pendingRequests.delete(String(data.id)); - clearTimeout(pendingRequest.timeoutId); - - // Send response back to the app (if it was expecting one) - this.dispatchMessageToIframe({ - type: 'ui-message-response', - messageId: pendingRequest.messageId, // The original message ID from the App - payload: { - messageId: pendingRequest.messageId, - response: data.result, - error: data.error, - }, - }); - } } } /** * Handles messages coming from the App (MCP-UI) and translates them to Host (JSON-RPC) - * + * * MCP-UI message types translated to MCP Apps SEP: * - 'tool' -> tools/call request * - 'ui-size-change' -> ui/notifications/size-changed notification @@ -454,167 +437,157 @@ class McpAppsAdapter { // Acknowledge receipt immediately this.dispatchMessageToIframe({ - type: 'ui-message-received', - payload: { messageId }, + type: 'ui-message-received', + payload: { messageId } }); try { - switch (message.type) { - // MCP-UI tool call -> MCP Apps tools/call - case 'tool': { - const { toolName, params } = (message as UIActionResult).payload as { - toolName: string; - params: unknown; - }; - const jsonRpcId = this.generateJsonRpcId(); - - this.pendingRequests.set(String(jsonRpcId), { - messageId, - type: 'tool', - resolve: () => {}, // Handled in handleHostMessage - reject: () => {}, - timeoutId: setTimeout(() => { - this.pendingRequests.delete(String(jsonRpcId)); - this.dispatchMessageToIframe({ - type: 'ui-message-response', - messageId, - payload: { messageId, error: 'Timeout' }, - }); - }, this.config.timeout), - }); - - this.sendJsonRpcRequest(jsonRpcId, METHODS.TOOLS_CALL, { - name: toolName, - arguments: params, - }); - break; - } - - // MCP-UI size change -> MCP Apps ui/notifications/size-changed - case 'ui-size-change': { - const { width, height } = ( - message as MCPUIMessage & { payload: { width?: number; height?: number } } - ).payload; - this.sendJsonRpcNotification(METHODS.SIZE_CHANGED, { width, height }); - break; - } - - // MCP-UI notification -> MCP Apps notifications/message (logging) - case 'notify': { - const { message: msg } = (message as UIActionResult).payload as { message: string }; - this.sendJsonRpcNotification(METHODS.NOTIFICATIONS_MESSAGE, { - level: 'info', - data: msg, - }); - break; - } - - // MCP-UI link -> MCP Apps ui/open-link request - case 'link': { - const { url } = (message as UIActionResult).payload as { url: string }; - const jsonRpcId = this.generateJsonRpcId(); - - this.pendingRequests.set(String(jsonRpcId), { - messageId, - type: 'link', - resolve: () => {}, - reject: () => {}, - timeoutId: setTimeout(() => { - this.pendingRequests.delete(String(jsonRpcId)); - this.dispatchMessageToIframe({ - type: 'ui-message-response', - messageId, - payload: { messageId, error: 'Timeout' }, - }); - }, this.config.timeout), - }); - - this.sendJsonRpcRequest(jsonRpcId, METHODS.OPEN_LINK, { url }); - break; + switch (message.type) { + // MCP-UI tool call -> MCP Apps tools/call + case 'tool': { + const { toolName, params } = (message as UIActionResult).payload as { toolName: string; params: unknown }; + const jsonRpcId = this.generateJsonRpcId(); + + this.pendingRequests.set(String(jsonRpcId), { + messageId, + type: 'tool', + resolve: () => {}, // Handled in handleHostMessage + reject: () => {}, + timeoutId: setTimeout(() => { + this.pendingRequests.delete(String(jsonRpcId)); + this.dispatchMessageToIframe({ + type: 'ui-message-response', + messageId, + payload: { messageId, error: 'Timeout' } + }) + }, this.config.timeout) + }); + + this.sendJsonRpcRequest(jsonRpcId, METHODS.TOOLS_CALL, { + name: toolName, + arguments: params + }); + break; + } + + // MCP-UI size change -> MCP Apps ui/notifications/size-changed + case 'ui-size-change': { + const { width, height } = (message as MCPUIMessage & { payload: { width?: number; height?: number } }).payload; + this.sendJsonRpcNotification(METHODS.SIZE_CHANGED, { width, height }); + break; + } + + // MCP-UI notification -> MCP Apps notifications/message (logging) + case 'notify': { + const { message: msg } = (message as UIActionResult).payload as { message: string }; + this.sendJsonRpcNotification(METHODS.NOTIFICATIONS_MESSAGE, { + level: 'info', + data: msg + }); + break; + } + + // MCP-UI link -> MCP Apps ui/open-link request + case 'link': { + const { url } = (message as UIActionResult).payload as { url: string }; + const jsonRpcId = this.generateJsonRpcId(); + + this.pendingRequests.set(String(jsonRpcId), { + messageId, + type: 'link', + resolve: () => {}, + reject: () => {}, + timeoutId: setTimeout(() => { + this.pendingRequests.delete(String(jsonRpcId)); + this.dispatchMessageToIframe({ + type: 'ui-message-response', + messageId, + payload: { messageId, error: 'Timeout' } + }) + }, this.config.timeout) + }); + + this.sendJsonRpcRequest(jsonRpcId, METHODS.OPEN_LINK, { url }); + break; + } + + // MCP-UI prompt -> MCP Apps ui/message request + case 'prompt': { + const { prompt } = (message as UIActionResult).payload as { prompt: string }; + const jsonRpcId = this.generateJsonRpcId(); + + this.pendingRequests.set(String(jsonRpcId), { + messageId, + type: 'prompt', + resolve: () => {}, + reject: () => {}, + timeoutId: setTimeout(() => { + this.pendingRequests.delete(String(jsonRpcId)); + this.dispatchMessageToIframe({ + type: 'ui-message-response', + messageId, + payload: { messageId, error: 'Timeout' } + }) + }, this.config.timeout) + }); + + this.sendJsonRpcRequest(jsonRpcId, METHODS.MESSAGE, { + role: 'user', + content: [{ type: 'text', text: prompt }] + }); + break; + } + + // MCP-UI iframe ready -> MCP Apps ui/notifications/initialized + case 'ui-lifecycle-iframe-ready': { + this.sendJsonRpcNotification(METHODS.INITIALIZED, {}); + // Also send current render data (like Apps SDK) + this.sendRenderData(); + break; + } + + // MCP-UI request render data -> Send current render data + case 'ui-request-render-data': { + this.sendRenderData(messageId); + break; + } + + // MCP-UI intent -> Currently no direct equivalent in MCP Apps + // We translate it to a ui/message with the intent description + case 'intent': { + const { intent, params } = (message as UIActionResult).payload as { intent: string; params: unknown }; + const jsonRpcId = this.generateJsonRpcId(); + + this.pendingRequests.set(String(jsonRpcId), { + messageId, + type: 'intent', + resolve: () => {}, + reject: () => {}, + timeoutId: setTimeout(() => { + this.pendingRequests.delete(String(jsonRpcId)); + this.dispatchMessageToIframe({ + type: 'ui-message-response', + messageId, + payload: { messageId, error: 'Timeout' } + }) + }, this.config.timeout) + }); + + // Translate intent to a message + this.sendJsonRpcRequest(jsonRpcId, METHODS.MESSAGE, { + role: 'user', + content: [{ type: 'text', text: `Intent: ${intent}. Parameters: ${JSON.stringify(params)}` }], + }); + break; + } } - - // MCP-UI prompt -> MCP Apps ui/message request - case 'prompt': { - const { prompt } = (message as UIActionResult).payload as { prompt: string }; - const jsonRpcId = this.generateJsonRpcId(); - - this.pendingRequests.set(String(jsonRpcId), { - messageId, - type: 'prompt', - resolve: () => {}, - reject: () => {}, - timeoutId: setTimeout(() => { - this.pendingRequests.delete(String(jsonRpcId)); - this.dispatchMessageToIframe({ - type: 'ui-message-response', - messageId, - payload: { messageId, error: 'Timeout' }, - }); - }, this.config.timeout), - }); - - this.sendJsonRpcRequest(jsonRpcId, METHODS.MESSAGE, { - role: 'user', - content: [{ type: 'text', text: prompt }], - }); - break; - } - - // MCP-UI iframe ready -> MCP Apps ui/notifications/initialized - case 'ui-lifecycle-iframe-ready': { - this.sendJsonRpcNotification(METHODS.INITIALIZED, {}); - // Also send current render data (like Apps SDK) - this.sendRenderData(); - break; - } - - // MCP-UI request render data -> Send current render data - case 'ui-request-render-data': { - this.sendRenderData(messageId); - break; - } - - // MCP-UI intent -> Currently no direct equivalent in MCP Apps - // We translate it to a ui/message with the intent description - case 'intent': { - const { intent, params } = (message as UIActionResult).payload as { - intent: string; - params: unknown; - }; - const jsonRpcId = this.generateJsonRpcId(); - - this.pendingRequests.set(String(jsonRpcId), { - messageId, - type: 'intent', - resolve: () => {}, - reject: () => {}, - timeoutId: setTimeout(() => { - this.pendingRequests.delete(String(jsonRpcId)); - this.dispatchMessageToIframe({ - type: 'ui-message-response', - messageId, - payload: { messageId, error: 'Timeout' }, - }); - }, this.config.timeout), - }); - - // Translate intent to a message - this.sendJsonRpcRequest(jsonRpcId, METHODS.MESSAGE, { - role: 'user', - content: [ - { type: 'text', text: `Intent: ${intent}. Parameters: ${JSON.stringify(params)}` }, - ], - }); - break; - } - } } catch (error) { - this.config.logger.error('[MCP Apps Adapter] Error handling message:', error); - this.dispatchMessageToIframe({ - type: 'ui-message-response', - messageId, - payload: { messageId, error }, - }); + this.config.logger.error('[MCP Apps Adapter] Error handling message:', error); + this.dispatchMessageToIframe({ + type: 'ui-message-response', + messageId, + payload: { messageId, error } + }); } } @@ -635,47 +608,34 @@ class McpAppsAdapter { theme: this.currentRenderData.theme, displayMode: this.currentRenderData.displayMode, maxHeight: this.currentRenderData.maxHeight, - }, - }, + } + } }); } - private sendJsonRpcRequest( - id: number | string, - method: string, - params?: Record, - ) { - this.originalPostMessage?.( - { - jsonrpc: '2.0', - id, - method, - params, - }, - '*', - ); + private sendJsonRpcRequest(id: number | string, method: string, params?: Record) { + this.originalPostMessage?.({ + jsonrpc: '2.0', + id, + method, + params + }, '*'); } private sendJsonRpcResponse(id: number | string, result: Record) { - this.originalPostMessage?.( - { - jsonrpc: '2.0', - id, - result, - }, - '*', - ); + this.originalPostMessage?.({ + jsonrpc: '2.0', + id, + result + }, '*'); } private sendJsonRpcNotification(method: string, params?: Record) { - this.originalPostMessage?.( - { - jsonrpc: '2.0', - method, - params, - }, - '*', - ); + this.originalPostMessage?.({ + jsonrpc: '2.0', + method, + params + }, '*'); } private dispatchMessageToIframe(data: MCPUIMessage): void { @@ -690,9 +650,9 @@ class McpAppsAdapter { private generateMessageId(): string { return `adapter-${Date.now()}-${++this.messageIdCounter}`; } - + private generateJsonRpcId(): number { - return ++this.messageIdCounter; + return ++this.messageIdCounter; } } @@ -715,3 +675,4 @@ function uninstallAdapter(): void { adapterInstance = null; } } + From 2238a9fb3048699536867cbb2299ef9789cda70e Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Thu, 18 Dec 2025 18:26:18 +0200 Subject: [PATCH 3/4] fix: lint --- .eslintignore | 8 ----- .eslintrc.json | 33 ------------------- docs/src/.vitepress/config.ts | 2 +- docs/src/.vitepress/theme/index.ts | 2 +- package.json | 4 +-- .../src/adapters/mcp-apps/adapter-runtime.ts | 3 +- sdks/typescript/server/src/utils.ts | 2 +- sdks/typescript/server/vite.config.ts | 1 - 8 files changed, 6 insertions(+), 49 deletions(-) delete mode 100644 .eslintignore delete mode 100644 .eslintrc.json diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index f3af2557..00000000 --- a/.eslintignore +++ /dev/null @@ -1,8 +0,0 @@ -node_modules -dist -coverage -*.log -**/*.js -sdks/typescript/client/src/remote-dom/iframe-bundle.ts - -examples \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 9a4ebbef..00000000 --- a/.eslintrc.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "root": true, - "parser": "@typescript-eslint/parser", - "plugins": ["@typescript-eslint", "react"], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:react/recommended", - "plugin:react/jsx-runtime", - "prettier" - ], - "settings": { - "react": { - "version": "detect" - } - }, - "env": { - "node": true, - "browser": true, - "es2021": true, - "jest": true - }, - "parserOptions": { - "ecmaVersion": "latest", - "sourceType": "module", - "ecmaFeatures": { - "jsx": true - } - }, - "rules": { - "react/prop-types": "off" - } -} diff --git a/docs/src/.vitepress/config.ts b/docs/src/.vitepress/config.ts index eabee191..d653dddf 100644 --- a/docs/src/.vitepress/config.ts +++ b/docs/src/.vitepress/config.ts @@ -319,7 +319,7 @@ export default withMermaid( dark: 'github-dark', }, lineNumbers: true, - config: (md) => { + config: (_md) => { // Add any markdown-it plugins here }, }, diff --git a/docs/src/.vitepress/theme/index.ts b/docs/src/.vitepress/theme/index.ts index 09015433..cd90e6c3 100644 --- a/docs/src/.vitepress/theme/index.ts +++ b/docs/src/.vitepress/theme/index.ts @@ -15,7 +15,7 @@ export default { ]) }) }, - enhanceApp({ app, router, siteData }) { + enhanceApp({ app: _app, router: _router, siteData: _siteData }) { // Custom app enhancements can go here }, } satisfies Theme; diff --git a/package.json b/package.json index 9c280320..c60276ba 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,8 @@ "test:ruby": "cd sdks/ruby && bundle install && bundle exec rake spec", "test:watch": "vitest watch", "coverage": "vitest run --coverage", - "lint": "eslint . --ext .ts,.tsx,.js,.jsx", - "lint:fix": "eslint . --ext .ts,.tsx,.js,.jsx --fix", + "lint": "eslint .", + "lint:fix": "eslint . --fix", "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"", "version:patch": "echo \"Versioning with Changesets, then run pnpm install\" && pnpm changeset version && pnpm install --lockfile-only", "publish-packages": "pnpm build && pnpm changeset publish", diff --git a/sdks/typescript/server/src/adapters/mcp-apps/adapter-runtime.ts b/sdks/typescript/server/src/adapters/mcp-apps/adapter-runtime.ts index 32bece29..69648fa1 100644 --- a/sdks/typescript/server/src/adapters/mcp-apps/adapter-runtime.ts +++ b/sdks/typescript/server/src/adapters/mcp-apps/adapter-runtime.ts @@ -45,7 +45,6 @@ const LATEST_PROTOCOL_VERSION = '2025-11-21'; * * @see https://github.com/modelcontextprotocol/ext-apps/blob/main/src/spec.types.ts */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -- Used in switch cases below const METHODS = { // Lifecycle INITIALIZE: 'ui/initialize', @@ -234,7 +233,7 @@ class McpAppsAdapter { try { await initPromise; - } catch (error) { + } catch (_error) { // Initialization failed, but we still try to work this.config.logger.warn('[MCP Apps Adapter] Continuing despite initialization error'); } diff --git a/sdks/typescript/server/src/utils.ts b/sdks/typescript/server/src/utils.ts index d5dadd7e..b823b5a5 100644 --- a/sdks/typescript/server/src/utils.ts +++ b/sdks/typescript/server/src/utils.ts @@ -55,7 +55,7 @@ export function utf8ToBase64(str: string): string { ); try { return btoa(str); - } catch (e) { + } catch (_e) { throw new Error( 'MCP-UI SDK: Suitable UTF-8 to Base64 encoding method not found, and fallback btoa failed.', ); diff --git a/sdks/typescript/server/vite.config.ts b/sdks/typescript/server/vite.config.ts index 2b8b7ebe..4dcbb46f 100644 --- a/sdks/typescript/server/vite.config.ts +++ b/sdks/typescript/server/vite.config.ts @@ -11,7 +11,6 @@ export default defineConfig({ tsconfigPath: path.resolve(__dirname, 'tsconfig.json'), exclude: ['**/__tests__/**', '**/*.test.ts', '**/*.spec.ts'], }), - // eslint-disable-next-line @typescript-eslint/no-explicit-any ], build: { lib: { From 5298cf964334aa0cda94b9e2f4d3516cba8d3011 Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Thu, 18 Dec 2025 18:27:49 +0200 Subject: [PATCH 4/4] lint --- eslint.config.js | 191 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 eslint.config.js diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000..b3f9d0b8 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,191 @@ +const tseslint = require('@typescript-eslint/eslint-plugin'); +const tsparser = require('@typescript-eslint/parser'); +const react = require('eslint-plugin-react'); +const prettierConfig = require('eslint-config-prettier'); + +module.exports = [ + // Global ignores + { + ignores: [ + '**/node_modules/**', + '**/dist/**', + '**/coverage/**', + '**/*.log', + '**/*.js', + 'sdks/typescript/client/src/remote-dom/iframe-bundle.ts', + 'examples/**', + ], + }, + + // Base configuration for all files + { + files: ['**/*.ts', '**/*.tsx'], + languageOptions: { + parser: tsparser, + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, + }, + globals: { + // Node globals + console: 'readonly', + process: 'readonly', + Buffer: 'readonly', + __dirname: 'readonly', + __filename: 'readonly', + module: 'readonly', + require: 'readonly', + exports: 'readonly', + global: 'readonly', + setTimeout: 'readonly', + clearTimeout: 'readonly', + setInterval: 'readonly', + clearInterval: 'readonly', + setImmediate: 'readonly', + clearImmediate: 'readonly', + // Browser globals + window: 'readonly', + Window: 'readonly', + document: 'readonly', + navigator: 'readonly', + localStorage: 'readonly', + sessionStorage: 'readonly', + fetch: 'readonly', + btoa: 'readonly', + atob: 'readonly', + URL: 'readonly', + URLSearchParams: 'readonly', + customElements: 'readonly', + // DOM types + HTMLElement: 'readonly', + HTMLDivElement: 'readonly', + HTMLSpanElement: 'readonly', + HTMLButtonElement: 'readonly', + HTMLImageElement: 'readonly', + HTMLIFrameElement: 'readonly', + Element: 'readonly', + Node: 'readonly', + Event: 'readonly', + MessageEvent: 'readonly', + CustomEvent: 'readonly', + // Web APIs + TextEncoder: 'readonly', + TextDecoder: 'readonly', + Console: 'readonly', + Transferable: 'readonly', + WindowPostMessageOptions: 'readonly', + // ES2021 globals + Promise: 'readonly', + Symbol: 'readonly', + WeakMap: 'readonly', + WeakSet: 'readonly', + Proxy: 'readonly', + Reflect: 'readonly', + // Jest/Vitest globals + describe: 'readonly', + it: 'readonly', + test: 'readonly', + expect: 'readonly', + beforeEach: 'readonly', + afterEach: 'readonly', + beforeAll: 'readonly', + afterAll: 'readonly', + jest: 'readonly', + vi: 'readonly', + }, + }, + plugins: { + '@typescript-eslint': tseslint, + react: react, + }, + rules: { + // ESLint recommended rules (manually specified to avoid needing @eslint/js) + 'constructor-super': 'error', + 'for-direction': 'error', + 'getter-return': 'error', + 'no-async-promise-executor': 'error', + 'no-case-declarations': 'error', + 'no-class-assign': 'error', + 'no-compare-neg-zero': 'error', + 'no-cond-assign': 'error', + 'no-const-assign': 'error', + 'no-constant-condition': 'error', + 'no-control-regex': 'error', + 'no-debugger': 'error', + 'no-delete-var': 'error', + 'no-dupe-args': 'error', + 'no-dupe-class-members': 'error', + 'no-dupe-else-if': 'error', + 'no-dupe-keys': 'error', + 'no-duplicate-case': 'error', + 'no-empty': 'error', + 'no-empty-character-class': 'error', + 'no-empty-pattern': 'error', + 'no-ex-assign': 'error', + 'no-extra-boolean-cast': 'error', + 'no-fallthrough': 'error', + 'no-func-assign': 'error', + 'no-global-assign': 'error', + 'no-import-assign': 'error', + 'no-inner-declarations': 'error', + 'no-invalid-regexp': 'error', + 'no-irregular-whitespace': 'error', + 'no-loss-of-precision': 'error', + 'no-misleading-character-class': 'error', + 'no-mixed-spaces-and-tabs': 'error', + 'no-new-symbol': 'error', + 'no-nonoctal-decimal-escape': 'error', + 'no-obj-calls': 'error', + 'no-octal': 'error', + 'no-prototype-builtins': 'error', + 'no-redeclare': 'error', + 'no-regex-spaces': 'error', + 'no-self-assign': 'error', + 'no-setter-return': 'error', + 'no-shadow-restricted-names': 'error', + 'no-sparse-arrays': 'error', + 'no-this-before-super': 'error', + 'no-undef': 'error', + 'no-unexpected-multiline': 'error', + 'no-unreachable': 'error', + 'no-unsafe-finally': 'error', + 'no-unsafe-negation': 'error', + 'no-unsafe-optional-chaining': 'error', + 'no-unused-labels': 'error', + 'no-unused-vars': 'error', + 'no-useless-backreference': 'error', + 'no-useless-catch': 'error', + 'no-useless-escape': 'error', + 'no-with': 'error', + 'require-yield': 'error', + 'use-isnan': 'error', + 'valid-typeof': 'error', + + // TypeScript ESLint recommended rules + ...tseslint.configs.recommended.rules, + + // React recommended rules + ...react.configs.recommended.rules, + ...react.configs['jsx-runtime'].rules, + + // Custom overrides + 'react/prop-types': 'off', + '@typescript-eslint/no-unused-vars': ['error', { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_' + }], + }, + settings: { + react: { + version: 'detect', + }, + }, + }, + + // Prettier config (should be last to override other formatting rules) + prettierConfig, +];