From 8d736a7dab6a3d8b4caa1e3b3c3f335bc241c848 Mon Sep 17 00:00:00 2001 From: Ruben Casas Date: Mon, 27 Oct 2025 21:21:12 +0000 Subject: [PATCH 1/3] feat: refactor and normalise props --- README.md | 2 + docs/src/guide/client/html-resource.md | 4 + docs/src/guide/client/resource-renderer.md | 5 ++ sdks/typescript/client/README.md | 2 + .../src/components/HTMLResourceRenderer.tsx | 31 +++---- .../src/components/UIResourceRenderer.tsx | 20 +++-- .../__tests__/HTMLResourceRenderer.test.tsx | 38 ++++++++- .../__tests__/UIResourceRenderer.test.tsx | 44 ++++++++-- sdks/typescript/client/src/types.ts | 13 +++ .../utils/__tests__/processResource.test.ts | 66 +++++++++++++-- .../client/src/utils/processResource.ts | 84 ++++++++++++++----- 11 files changed, 250 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 07a9e52d..1737267c 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,8 @@ It accepts the following props: - **`iframeProps`**: Optional props passed to the iframe element - **`iframeRenderData`**: Optional `Record` to pass data to the iframe upon rendering. This enables advanced use cases where the parent application needs to provide initial state or configuration to the sandboxed iframe content. - **`autoResizeIframe`**: Optional `boolean | { width?: boolean; height?: boolean }` to automatically resize the iframe to the size of the content. +- **`mcpContextProps`**: Optional MCP invocation context forwarded to HTML resources (e.g., `toolInput`, `toolOutput`, `toolName`, `toolResponseMetadata`). Providing `toolOutput` here overrides `iframeRenderData` for the sandbox payload. +- **`clientContextProps`**: Optional host context (e.g., `theme`, `userAgent`, `model`) exposed to sandboxed HTML content. Both context props can also be supplied via `htmlProps` when you need HTML-specific overrides. - **`remoteDomProps`**: Optional props for the internal `` - **`library`**: Optional component library for Remote DOM resources (defaults to `basicComponentLibrary`) - **`remoteElements`**: remote element definitions for Remote DOM resources. diff --git a/docs/src/guide/client/html-resource.md b/docs/src/guide/client/html-resource.md index edb20ca9..e4496331 100644 --- a/docs/src/guide/client/html-resource.md +++ b/docs/src/guide/client/html-resource.md @@ -6,6 +6,7 @@ The `` component is an internal component used by `; @@ -13,6 +14,8 @@ export interface HTMLResourceRendererProps { style?: React.CSSProperties; proxy?: string; iframeRenderData?: Record; + mcpContextProps?: MCPContextProps; + clientContextProps?: ClientContextProps; autoResizeIframe?: boolean | { width?: boolean; height?: boolean }; sandboxPermissions?: string; iframeProps?: Omit, 'src' | 'srcDoc' | 'ref' | 'style'>; @@ -40,6 +43,7 @@ The component accepts the following props: - **`style`**: (Optional) Custom styles for the iframe. - **`proxy`**: (Optional) A URL to a proxy script. This is useful for hosts with a strict Content Security Policy (CSP). When provided, external URLs will be rendered in a nested iframe hosted at this URL. For example, if `proxy` is `https://my-proxy.com/`, the final URL will be `https://my-proxy.com/?url=`. For your convenience, mcp-ui hosts a proxy script at `https://proxy.mcpui.dev`, which you can use as a the prop value without any setup (see `examples/external-url-demo`). - **`iframeProps`**: (Optional) Custom props for the iframe. +- **`iframeRenderData`**: (Optional) Additional data merged into the render payload forwarded to the iframe. When `mcpContextProps.toolOutput` is provided, it takes precedence over this merged object. - **`autoResizeIframe`**: (Optional) When enabled, the iframe will automatically resize based on messages from the iframe's content. This prop can be a boolean (to enable both width and height resizing) or an object (`{width?: boolean, height?: boolean}`) to control dimensions independently. - **`sandboxPermissions`**: (Optional) Additional iframe sandbox permissions to add to the defaults. These are merged with: - External URLs (`text/uri-list`): `'allow-scripts allow-same-origin'` diff --git a/docs/src/guide/client/resource-renderer.md b/docs/src/guide/client/resource-renderer.md index 2c1c9956..bd91a2e1 100644 --- a/docs/src/guide/client/resource-renderer.md +++ b/docs/src/guide/client/resource-renderer.md @@ -42,6 +42,7 @@ The `UIResourceRenderer` automatically detects and uses metadata from resources ```typescript import type { Resource } from '@modelcontextprotocol/sdk/types'; +import type { MCPContextProps, ClientContextProps } from '@mcp-ui/client'; interface UIResourceRendererProps { resource: Partial; @@ -49,6 +50,8 @@ interface UIResourceRendererProps { supportedContentTypes?: ResourceContentType[]; htmlProps?: Omit; remoteDomProps?: Omit; + mcpContextProps?: MCPContextProps; + clientContextProps?: ClientContextProps; } ``` @@ -76,6 +79,8 @@ interface UIResourceRendererProps { - **`ref`**: Optional React ref to access the underlying iframe element - **`iframeRenderData`**: Optional `Record` to pass data to the iframe upon rendering. This enables advanced use cases where the parent application needs to provide initial state or configuration to the sandboxed iframe content. - **`autoResizeIframe`**: Optional `boolean | { width?: boolean; height?: boolean }` to automatically resize the iframe to the size of the content. +- **`mcpContextProps`**: Optional MCP invocation context forwarded to HTML resources (e.g., `toolInput`, `toolOutput`, `toolName`, `toolResponseMetadata`). These can also be provided via `htmlProps` for HTML-only overrides. +- **`clientContextProps`**: Optional host context for HTML resources (e.g., `theme`, `userAgent`, `model`). When unspecified, defaults are used. Like `mcpContextProps`, these can be supplied in `htmlProps` to scope them to HTML resources. - **`remoteDomProps`**: Optional props for the `` - **`library`**: Optional component library for Remote DOM resources (defaults to `basicComponentLibrary`) - **`remoteElements`**: Optional remote element definitions for Remote DOM resources. REQUIRED for Remote DOM snippets. diff --git a/sdks/typescript/client/README.md b/sdks/typescript/client/README.md index 07a9e52d..1737267c 100644 --- a/sdks/typescript/client/README.md +++ b/sdks/typescript/client/README.md @@ -98,6 +98,8 @@ It accepts the following props: - **`iframeProps`**: Optional props passed to the iframe element - **`iframeRenderData`**: Optional `Record` to pass data to the iframe upon rendering. This enables advanced use cases where the parent application needs to provide initial state or configuration to the sandboxed iframe content. - **`autoResizeIframe`**: Optional `boolean | { width?: boolean; height?: boolean }` to automatically resize the iframe to the size of the content. +- **`mcpContextProps`**: Optional MCP invocation context forwarded to HTML resources (e.g., `toolInput`, `toolOutput`, `toolName`, `toolResponseMetadata`). Providing `toolOutput` here overrides `iframeRenderData` for the sandbox payload. +- **`clientContextProps`**: Optional host context (e.g., `theme`, `userAgent`, `model`) exposed to sandboxed HTML content. Both context props can also be supplied via `htmlProps` when you need HTML-specific overrides. - **`remoteDomProps`**: Optional props for the internal `` - **`library`**: Optional component library for Remote DOM resources (defaults to `basicComponentLibrary`) - **`remoteElements`**: remote element definitions for Remote DOM resources. diff --git a/sdks/typescript/client/src/components/HTMLResourceRenderer.tsx b/sdks/typescript/client/src/components/HTMLResourceRenderer.tsx index ca267c56..a2c5abf0 100644 --- a/sdks/typescript/client/src/components/HTMLResourceRenderer.tsx +++ b/sdks/typescript/client/src/components/HTMLResourceRenderer.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useImperativeHandle, useMemo, useRef } from 'react'; import type { Resource } from '@modelcontextprotocol/sdk/types.js'; -import { UIActionResult, UIMetadataKey } from '../types'; +import { UIActionResult, UIMetadataKey, MCPContextProps, ClientContextProps } from '../types'; import { processHTMLResource } from '../utils/processResource'; import { getUIResourceMetadata } from '../utils/metadataUtils'; @@ -10,9 +10,8 @@ export type HTMLResourceRendererProps = { style?: React.CSSProperties; proxy?: string; iframeRenderData?: Record; - toolInput?: Record; - toolName?: string; - toolResponseMetadata?: Record; + mcpContextProps?: MCPContextProps; + clientContextProps?: ClientContextProps; autoResizeIframe?: boolean | { width?: boolean; height?: boolean }; sandboxPermissions?: string; iframeProps?: Omit, 'src' | 'srcDoc' | 'style'> & { @@ -46,9 +45,8 @@ export const HTMLResourceRenderer = ({ style, proxy, iframeRenderData, - toolInput, - toolName, - toolResponseMetadata, + mcpContextProps, + clientContextProps, autoResizeIframe, sandboxPermissions, iframeProps, @@ -60,7 +58,7 @@ export const HTMLResourceRenderer = ({ const preferredFrameSize = uiMetadata[UIMetadataKey.PREFERRED_FRAME_SIZE] ?? ['100%', '100%']; const metadataInitialRenderData = uiMetadata[UIMetadataKey.INITIAL_RENDER_DATA] ?? undefined; - const initialRenderData = useMemo(() => { + const combinedRenderData = useMemo(() => { if (!iframeRenderData && !metadataInitialRenderData) { return undefined; } @@ -70,17 +68,20 @@ export const HTMLResourceRenderer = ({ }; }, [iframeRenderData, metadataInitialRenderData]); + const initialRenderData = useMemo( + () => mcpContextProps?.toolOutput ?? combinedRenderData, + [mcpContextProps?.toolOutput, combinedRenderData], + ); + const { error, iframeSrc, iframeRenderMode, htmlString } = useMemo( () => - processHTMLResource( - resource, + processHTMLResource(resource, { proxy, initialRenderData, - toolInput, - toolName, - toolResponseMetadata - ), - [resource, proxy] + mcpContextProps, + clientContextProps, + }), + [resource, proxy, initialRenderData, mcpContextProps, clientContextProps] ); diff --git a/sdks/typescript/client/src/components/UIResourceRenderer.tsx b/sdks/typescript/client/src/components/UIResourceRenderer.tsx index eaa3ee4b..3a798245 100644 --- a/sdks/typescript/client/src/components/UIResourceRenderer.tsx +++ b/sdks/typescript/client/src/components/UIResourceRenderer.tsx @@ -1,5 +1,5 @@ import type { EmbeddedResource } from '@modelcontextprotocol/sdk/types.js'; -import { ResourceContentType, UIActionResult } from '../types'; +import { ResourceContentType, UIActionResult, ClientContextProps, MCPContextProps } from '../types'; import { HTMLResourceRenderer, HTMLResourceRendererProps } from './HTMLResourceRenderer'; import { RemoteDOMResourceProps, RemoteDOMResourceRenderer } from './RemoteDOMResourceRenderer'; import { basicComponentLibrary } from '../remote-dom/component-libraries/basic'; @@ -8,8 +8,10 @@ export type UIResourceRendererProps = { resource: Partial; onUIAction?: (result: UIActionResult) => Promise; supportedContentTypes?: ResourceContentType[]; - htmlProps?: Omit; - remoteDomProps?: Omit; + htmlProps?: Omit; + remoteDomProps?: RemoteDOMResourceProps; + mcpContextProps?: MCPContextProps; + clientContextProps?: ClientContextProps; }; function getContentType( @@ -34,7 +36,7 @@ function getContentType( } export const UIResourceRenderer = (props: UIResourceRendererProps) => { - const { resource, onUIAction, supportedContentTypes, htmlProps, remoteDomProps } = props; + const { resource, onUIAction, supportedContentTypes, htmlProps, remoteDomProps, mcpContextProps, clientContextProps } = props; const contentType = getContentType(resource); if (supportedContentTypes && contentType && !supportedContentTypes.includes(contentType)) { @@ -45,7 +47,15 @@ export const UIResourceRenderer = (props: UIResourceRendererProps) => { case 'rawHtml': case 'skybridge': case 'externalUrl': { - return ; + return ( + + ); } case 'remoteDom': return ( diff --git a/sdks/typescript/client/src/components/__tests__/HTMLResourceRenderer.test.tsx b/sdks/typescript/client/src/components/__tests__/HTMLResourceRenderer.test.tsx index db32bd5c..c18cdb7a 100644 --- a/sdks/typescript/client/src/components/__tests__/HTMLResourceRenderer.test.tsx +++ b/sdks/typescript/client/src/components/__tests__/HTMLResourceRenderer.test.tsx @@ -71,7 +71,7 @@ describe('HTMLResource component', () => { render(); expect( screen.getByText( - 'Resource must be of type text/html (for HTML content) or text/uri-list (for URL content).', + 'Resource must be of type text/html (for HTML content), text/html+skybridge, or text/uri-list (for URL content).', ), ).toBeInTheDocument(); }); @@ -359,7 +359,7 @@ describe('HTMLResource iframe communication', () => { // Error message should be displayed expect( await screen.findByText( - 'Resource must be of type text/html (for HTML content) or text/uri-list (for URL content).', + 'Resource must be of type text/html (for HTML content), text/html+skybridge, or text/uri-list (for URL content).', ), ).toBeInTheDocument(); @@ -534,6 +534,40 @@ describe('HTMLResource metadata', () => { ); }); + it('should prioritize toolOutput from mcpContextProps over other render data', () => { + const iframeRenderData = { priority: 'iframe', foo: 'bar' }; + const metadataInitialRenderData = { priority: 'metadata', baz: 'qux' }; + const toolOutput = { priority: 'context', extra: 'value' }; + const resource = { + mimeType: 'text/uri-list', + text: 'https://example.com/app', + _meta: { [`${UI_METADATA_PREFIX}initial-render-data`]: metadataInitialRenderData }, + }; + const ref = React.createRef(); + render( + , + ); + expect(ref.current).toBeInTheDocument(); + const iframeWindow = ref.current?.contentWindow as Window; + const spy = vi.spyOn(iframeWindow, 'postMessage'); + dispatchMessage(iframeWindow, { + type: InternalMessageType.UI_LIFECYCLE_IFRAME_READY, + }); + expect(spy).toHaveBeenCalledWith( + { + type: InternalMessageType.UI_LIFECYCLE_IFRAME_RENDER_DATA, + payload: { renderData: toolOutput }, + messageId: undefined, + }, + '*', + ); + }); + it('should respond to ui-request-render-data with render data', () => { const iframeRenderData = { theme: 'dark', user: { id: '123' } }; const resource = { diff --git a/sdks/typescript/client/src/components/__tests__/UIResourceRenderer.test.tsx b/sdks/typescript/client/src/components/__tests__/UIResourceRenderer.test.tsx index 248535ad..df6ba5ff 100644 --- a/sdks/typescript/client/src/components/__tests__/UIResourceRenderer.test.tsx +++ b/sdks/typescript/client/src/components/__tests__/UIResourceRenderer.test.tsx @@ -29,7 +29,7 @@ describe('', () => { render(); expect(screen.getByTestId('html-resource')).toBeInTheDocument(); expect(RemoteDOMResourceRenderer).not.toHaveBeenCalled(); - expect(HTMLResourceRenderer).toHaveBeenCalledWith({ resource }, {}); + expect(HTMLResourceRenderer).toHaveBeenCalledWith(expect.objectContaining({ resource }), {}); }); it('should render HTMLResourceRenderer for "text/uri-list" mimeType', () => { @@ -37,7 +37,7 @@ describe('', () => { render(); expect(screen.getByTestId('html-resource')).toBeInTheDocument(); expect(RemoteDOMResourceRenderer).not.toHaveBeenCalled(); - expect(HTMLResourceRenderer).toHaveBeenCalledWith({ resource }, {}); + expect(HTMLResourceRenderer).toHaveBeenCalledWith(expect.objectContaining({ resource }), {}); }); it('should render RemoteDOMResourceRenderer for "remote-dom" mimeType', () => { @@ -75,7 +75,7 @@ describe('', () => { render(); expect(screen.getByTestId('html-resource')).toBeInTheDocument(); expect(RemoteDOMResourceRenderer).not.toHaveBeenCalled(); - expect(HTMLResourceRenderer).toHaveBeenCalledWith({ resource }, {}); + expect(HTMLResourceRenderer).toHaveBeenCalledWith(expect.objectContaining({ resource }), {}); }); it('should pass proxy prop to HTMLResourceRenderer for external URLs', () => { @@ -85,7 +85,7 @@ describe('', () => { ); expect(screen.getByTestId('html-resource')).toBeInTheDocument(); expect(HTMLResourceRenderer).toHaveBeenCalledWith( - { resource, proxy: 'https://proxy.mcpui.dev/' }, + expect.objectContaining({ resource, proxy: 'https://proxy.mcpui.dev/' }), {}, ); }); @@ -97,7 +97,41 @@ describe('', () => { ); expect(screen.getByTestId('html-resource')).toBeInTheDocument(); expect(HTMLResourceRenderer).toHaveBeenCalledWith( - { resource, proxy: 'https://proxy.mcpui.dev/' }, + expect.objectContaining({ resource, proxy: 'https://proxy.mcpui.dev/' }), + {}, + ); + }); + + it('should forward context props to HTMLResourceRenderer', () => { + const resource = { ...baseResource, mimeType: 'text/html' }; + const mcpContextProps = { toolName: 'demo-tool', toolInput: { foo: 'bar' } }; + const clientContextProps = { theme: 'light', model: 'gpt-5' }; + render( + , + ); + expect(HTMLResourceRenderer).toHaveBeenCalledWith( + expect.objectContaining({ resource, mcpContextProps, clientContextProps }), + {}, + ); + }); + + it('should use htmlProps context when top-level context is undefined', () => { + const resource = { ...baseResource, mimeType: 'text/html' }; + const htmlProps = { + mcpContextProps: { toolName: 'html-only', toolInput: { bar: 'baz' } }, + clientContextProps: { theme: 'dark', userAgent: 'jest' }, + }; + render(); + expect(HTMLResourceRenderer).toHaveBeenCalledWith( + expect.objectContaining({ + resource, + mcpContextProps: htmlProps.mcpContextProps, + clientContextProps: htmlProps.clientContextProps, + }), {}, ); }); diff --git a/sdks/typescript/client/src/types.ts b/sdks/typescript/client/src/types.ts index 2cfe58bc..53c76f6a 100644 --- a/sdks/typescript/client/src/types.ts +++ b/sdks/typescript/client/src/types.ts @@ -102,3 +102,16 @@ export type UIResourceMetadata = { [UIMetadataKey.PREFERRED_FRAME_SIZE]?: [string, string]; [UIMetadataKey.INITIAL_RENDER_DATA]?: Record; }; + +export type MCPContextProps = { + toolInput?: Record; + toolOutput?: Record; + toolName?: string; + toolResponseMetadata?: Record; +}; + +export type ClientContextProps = { + theme?: string; + userAgent?: string; + model?: string; +}; diff --git a/sdks/typescript/client/src/utils/__tests__/processResource.test.ts b/sdks/typescript/client/src/utils/__tests__/processResource.test.ts index ee00b387..bb3b9b22 100644 --- a/sdks/typescript/client/src/utils/__tests__/processResource.test.ts +++ b/sdks/typescript/client/src/utils/__tests__/processResource.test.ts @@ -190,7 +190,7 @@ describe('processHTMLResource', () => { mimeType: 'text/uri-list', text: 'https://example.com', }; - const result = processHTMLResource(resource, 'https://proxy.mcpui.dev/'); + const result = processHTMLResource(resource, { proxy: 'https://proxy.mcpui.dev/' }); expect(result.error).toBeUndefined(); expect(result.iframeSrc).toBe('https://proxy.mcpui.dev/?url=https%3A%2F%2Fexample.com'); expect(result.iframeRenderMode).toBe('src'); @@ -201,7 +201,7 @@ describe('processHTMLResource', () => { mimeType: 'text/uri-list', text: 'https://example.com', }; - const result = processHTMLResource(resource, 'https://proxy.mcpui.dev/?a=1&b=2'); + const result = processHTMLResource(resource, { proxy: 'https://proxy.mcpui.dev/?a=1&b=2' }); expect(result.error).toBeUndefined(); expect(result.iframeSrc).toBe( 'https://proxy.mcpui.dev/?a=1&b=2&url=https%3A%2F%2Fexample.com', @@ -214,7 +214,7 @@ describe('processHTMLResource', () => { mimeType: 'text/uri-list', text: 'https://example.com', }; - const result = processHTMLResource(resource, 'not-a-valid-url'); + const result = processHTMLResource(resource, { proxy: 'not-a-valid-url' }); expect(result.error).toBeUndefined(); expect(result.iframeSrc).toBe('https://example.com'); expect(result.iframeRenderMode).toBe('src'); @@ -225,7 +225,7 @@ describe('processHTMLResource', () => { mimeType: 'text/uri-list', text: 'https://example.com', }; - const result = processHTMLResource(resource, ''); + const result = processHTMLResource(resource, { proxy: '' }); expect(result.error).toBeUndefined(); expect(result.iframeSrc).toBe('https://example.com'); expect(result.iframeRenderMode).toBe('src'); @@ -248,7 +248,7 @@ describe('processHTMLResource', () => { mimeType: 'text/html' as const, text: html, }; - const result = processHTMLResource(resource, 'https://proxy.mcpui.dev/'); + const result = processHTMLResource(resource, { proxy: 'https://proxy.mcpui.dev/' }); expect(result.error).toBeUndefined(); expect(result.iframeRenderMode).toBe('src'); expect(result.iframeSrc).toBe('https://proxy.mcpui.dev/?contentType=rawhtml'); @@ -262,7 +262,7 @@ describe('processHTMLResource', () => { mimeType: 'text/html' as const, blob: btoa(html), }; - const result = processHTMLResource(resource, 'https://proxy.mcpui.dev/'); + const result = processHTMLResource(resource, { proxy: 'https://proxy.mcpui.dev/' }); expect(result.error).toBeUndefined(); expect(result.iframeRenderMode).toBe('src'); expect(result.iframeSrc).toBe('https://proxy.mcpui.dev/?contentType=rawhtml'); @@ -276,7 +276,7 @@ describe('processHTMLResource', () => { mimeType: 'text/html' as const, text: html, }; - const result = processHTMLResource(resource, 'not-a-valid-url'); + const result = processHTMLResource(resource, { proxy: 'not-a-valid-url' }); expect(result.error).toBeUndefined(); expect(result.iframeRenderMode).toBe('srcDoc'); expect(result.htmlString).toBe(html); @@ -289,7 +289,7 @@ describe('processHTMLResource', () => { mimeType: 'text/html' as const, text: html, }; - const result = processHTMLResource(resource, ''); + const result = processHTMLResource(resource, { proxy: '' }); expect(result.error).toBeUndefined(); expect(result.iframeRenderMode).toBe('srcDoc'); expect(result.htmlString).toBe(html); @@ -309,6 +309,54 @@ describe('processHTMLResource', () => { expect(result.iframeSrc).toBeUndefined(); }); }); + + it('should inject MCP and client context into skybridge HTML', () => { + const resource = { + mimeType: 'text/html+skybridge' as const, + text: 'Test', + }; + const result = processHTMLResource(resource, { + initialRenderData: { fallback: true }, + mcpContextProps: { + toolInput: { foo: 'bar' }, + toolOutput: { override: 'yes' }, + toolName: 'demo-tool', + toolResponseMetadata: { traceId: 'abc' }, + }, + clientContextProps: { + theme: 'light', + userAgent: 'jest-agent', + model: 'gpt-test', + }, + }); + + expect(result.error).toBeUndefined(); + expect(result.htmlString).toContain('openai-widget-state:demo-tool:'); + expect(result.htmlString).toContain('toolInput: {"foo":"bar"}'); + expect(result.htmlString).toContain('toolOutput: {"override":"yes"}'); + expect(result.htmlString).toContain('toolResponseMetadata: {"traceId":"abc"}'); + expect(result.htmlString).toContain('toolName: "demo-tool"'); + expect(result.htmlString).toContain('theme: "light"'); + expect(result.htmlString).toContain('userAgent: "jest-agent"'); + expect(result.htmlString).toContain('model: "gpt-test"'); + }); + + it('should fall back to initialRenderData when toolOutput is undefined', () => { + const resource = { + mimeType: 'text/html+skybridge' as const, + text: '', + }; + const initialRenderData = { fallback: true }; + const result = processHTMLResource(resource, { + initialRenderData, + mcpContextProps: { + toolInput: { foo: 'bar' }, + }, + }); + + expect(result.error).toBeUndefined(); + expect(result.htmlString).toContain('toolOutput: {"fallback":true}'); + }); }); describe('Unsupported mimeType', () => { @@ -319,7 +367,7 @@ describe('processHTMLResource', () => { }; const result = processHTMLResource(resource); expect(result.error).toBe( - 'Resource must be of type text/html (for HTML content) or text/uri-list (for URL content).', + 'Resource must be of type text/html (for HTML content), text/html+skybridge, or text/uri-list (for URL content).', ); }); }); diff --git a/sdks/typescript/client/src/utils/processResource.ts b/sdks/typescript/client/src/utils/processResource.ts index 0c8b500f..edfde3dd 100644 --- a/sdks/typescript/client/src/utils/processResource.ts +++ b/sdks/typescript/client/src/utils/processResource.ts @@ -1,4 +1,5 @@ import type { Resource } from '@modelcontextprotocol/sdk/types.js'; +import type { ClientContextProps, MCPContextProps } from '../types'; type ProcessResourceResult = { error?: string; @@ -7,26 +8,43 @@ type ProcessResourceResult = { htmlString?: string; }; -const apiScript = ( - widgetStateKey: string, - toolInput?: Record, - initialRenderData?: Record, - toolResponseMetadata?: Record, -) => ` +type ApiScriptOptions = { + widgetStateKey: string; + toolInput?: Record; + toolOutput?: Record; + toolResponseMetadata?: Record; + toolName?: string; + theme?: string; + userAgent?: unknown; + model?: string; +}; + +const apiScript = ({ + widgetStateKey, + toolInput, + toolOutput, + toolResponseMetadata, + toolName, + theme, + userAgent, + model, +}: ApiScriptOptions) => `