From f6f0c6e023d3b50c7ee05a7db260472302eebe67 Mon Sep 17 00:00:00 2001 From: Andrew Harvard Date: Thu, 19 Feb 2026 15:57:53 -0500 Subject: [PATCH] fix(apps): restore MCP app sampling support reverted by #6933 (#7366) --- .../src/components/McpApps/McpAppRenderer.tsx | 49 ++++++++++++++++++- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/ui/desktop/src/components/McpApps/McpAppRenderer.tsx b/ui/desktop/src/components/McpApps/McpAppRenderer.tsx index 4733727359..cd1646fcbb 100644 --- a/ui/desktop/src/components/McpApps/McpAppRenderer.tsx +++ b/ui/desktop/src/components/McpApps/McpAppRenderer.tsx @@ -15,7 +15,7 @@ * - "standalone" — Goose-specific mode for dedicated Electron windows */ -import { AppRenderer } from '@mcp-ui/client'; +import { AppRenderer, type RequestHandlerExtra } from '@mcp-ui/client'; import type { McpUiDisplayMode, McpUiHostContext, @@ -23,7 +23,7 @@ import type { McpUiResourcePermissions, McpUiSizeChangedNotification, } from '@modelcontextprotocol/ext-apps/app-bridge'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { CallToolResult, JSONRPCRequest } from '@modelcontextprotocol/sdk/types.js'; import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'; import { callTool, readResource } from '../../api'; import { AppEvents } from '../../constants/events'; @@ -40,6 +40,8 @@ import { McpAppToolInputPartial, McpAppToolResult, DimensionLayout, + SamplingCreateMessageParams, + SamplingCreateMessageResponse, } from './types'; const DEFAULT_IFRAME_HEIGHT = 200; @@ -265,7 +267,13 @@ export default function McpAppRenderer({ const containerRef = useRef(null); const [containerWidth, setContainerWidth] = useState(0); const [containerHeight, setContainerHeight] = useState(0); + const [apiHost, setApiHost] = useState(null); + const [secretKey, setSecretKey] = useState(null); + useEffect(() => { + window.electron.getGoosedHostPort().then(setApiHost); + window.electron.getSecretKey().then(setSecretKey); + }, []); // Fetch the resource from the extension to get HTML and metadata (CSP, permissions, etc.). // If cachedHtml is provided we show it immediately; the fetch updates metadata and @@ -527,6 +535,42 @@ export default function McpAppRenderer({ return () => observer.disconnect(); }, []); + const handleFallbackRequest = useCallback( + async (request: JSONRPCRequest, _extra: RequestHandlerExtra) => { + if (request.method === 'sampling/createMessage') { + if (!sessionId || !apiHost || !secretKey) { + throw new Error('Session not initialized for sampling request'); + } + const { messages, systemPrompt, maxTokens } = + request.params as unknown as SamplingCreateMessageParams; + const response = await fetch(`${apiHost}/sessions/${sessionId}/sampling/message`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Secret-Key': secretKey, + }, + body: JSON.stringify({ + messages: messages.map((m) => ({ + role: m.role, + content: m.content, + })), + systemPrompt, + maxTokens, + }), + }); + if (!response.ok) { + throw new Error(`Sampling request failed: ${response.statusText}`); + } + return (await response.json()) as SamplingCreateMessageResponse; + } + return { + status: 'error' as const, + message: `Unhandled JSON-RPC method: ${request.method ?? ''}`, + }; + }, + [sessionId, apiHost, secretKey] + ); + const handleError = useCallback((err: Error) => { console.error('[MCP App Error]:', err); dispatch({ type: 'ERROR', message: errorMessage(err) }); @@ -643,6 +687,7 @@ export default function McpAppRenderer({ onReadResource={handleReadResource} onLoggingMessage={handleLoggingMessage} onSizeChanged={handleSizeChanged} + onFallbackRequest={handleFallbackRequest} onError={handleError} /> );