From 91848301c6e4f82f8b131f2fff875584195910d4 Mon Sep 17 00:00:00 2001 From: ochafik Date: Fri, 21 Nov 2025 22:29:12 +0000 Subject: [PATCH 01/29] UITemplatedToolCallRendererProps for MCP Apps --- pnpm-lock.yaml | 251 ++++++++- sdks/typescript/client/package.json | 5 +- .../UITemplatedToolCallRenderer.tsx | 484 ++++++++++++++++++ sdks/typescript/client/src/index.ts | 3 + 4 files changed, 730 insertions(+), 13 deletions(-) create mode 100644 sdks/typescript/client/src/components/UITemplatedToolCallRenderer.tsx diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9ea58da5..ab698f4b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,7 +37,7 @@ importers: version: 6.6.3 '@testing-library/react': specifier: ^14.1.2 - version: 14.3.1(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 14.3.1(@types/react@19.1.8)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@types/jest': specifier: ^29.5.11 version: 29.5.14 @@ -109,7 +109,7 @@ importers: version: 3.9.1(@types/node@22.15.33)(rollup@4.44.0)(typescript@5.8.3)(vite@5.4.19(@types/node@22.15.33)(lightningcss@1.30.1)) vitepress: specifier: ^1.0.0-rc.44 - version: 1.6.3(@algolia/client-search@5.29.0)(@types/node@22.15.33)(@types/react@19.1.8)(lightningcss@1.30.1)(postcss@8.5.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(search-insights@2.17.3)(typescript@5.8.3) + version: 1.6.3(@algolia/client-search@5.29.0)(@types/node@22.15.33)(@types/react@19.1.8)(lightningcss@1.30.1)(postcss@8.5.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(search-insights@2.17.3)(typescript@5.8.3) vitest: specifier: ^1.0.4 version: 1.6.1(@types/node@22.15.33)(jsdom@23.2.0)(lightningcss@1.30.1) @@ -382,6 +382,12 @@ importers: sdks/typescript/client: dependencies: + '@mcp-ui/shared': + specifier: workspace:* + version: link:../shared + '@modelcontextprotocol/ext-apps': + specifier: git+https://github.com/modelcontextprotocol/ext-apps.git + version: git+https://git@github.com:modelcontextprotocol/ext-apps.git#f53643bac71cad4997cc92b0ac955194bb298ff8 '@modelcontextprotocol/sdk': specifier: '*' version: 1.13.1 @@ -397,6 +403,9 @@ importers: '@remote-dom/react': specifier: ^1.2.2 version: 1.2.2(@preact/signals-core@1.10.0)(react@18.3.1) + zod: + specifier: ^3.23.8 + version: 3.25.67 devDependencies: '@testing-library/jest-dom': specifier: ^6.0.0 @@ -470,6 +479,13 @@ importers: version: 1.6.1(@types/node@18.19.112)(jsdom@23.2.0)(lightningcss@1.30.1) sdks/typescript/shared: + dependencies: + '@modelcontextprotocol/sdk': + specifier: '*' + version: 1.16.0 + zod: + specifier: ^3.23.8 + version: 3.25.67 devDependencies: typescript: specifier: ^5.0.0 @@ -1913,6 +1929,10 @@ packages: '@mjackson/node-fetch-server@0.6.1': resolution: {integrity: sha512-9ZJnk/DJjt805uv5PPv11haJIW+HHf3YEEyVXv+8iLQxLD/iXA68FH220XoiTPBC4gCg5q+IMadDw8qPqlA5wg==} + '@modelcontextprotocol/ext-apps@git+https://git@github.com:modelcontextprotocol/ext-apps.git#f53643bac71cad4997cc92b0ac955194bb298ff8': + resolution: {commit: f53643bac71cad4997cc92b0ac955194bb298ff8, repo: git@github.com:modelcontextprotocol/ext-apps.git, type: git} + version: 1.0.0 + '@modelcontextprotocol/sdk@1.13.1': resolution: {integrity: sha512-8q6+9aF0yA39/qWT/uaIj6zTpC+Qu07DnN/lb9mjoquCJsAh6l3HyYqc9O3t2j7GilseOQOQimLg7W3By6jqvg==} engines: {node: '>=18'} @@ -1921,6 +1941,15 @@ packages: resolution: {integrity: sha512-8ofX7gkZcLj9H9rSd50mCgm3SSF8C7XoclxJuLoV0Cz3rEQ1tv9MZRYYvJtm9n1BiEQQMzSmE/w2AEkNacLYfg==} engines: {node: '>=18'} + '@modelcontextprotocol/sdk@1.22.0': + resolution: {integrity: sha512-VUpl106XVTCpDmTBil2ehgJZjhyLY2QZikzF8NvTXtLRF1CvO5iEE2UNZdVIUer35vFOwMKYeUGbjJtvPWan3g==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + '@napi-rs/wasm-runtime@0.2.11': resolution: {integrity: sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==} @@ -2048,6 +2077,61 @@ packages: resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} + '@oven/bun-darwin-aarch64@1.3.3': + resolution: {integrity: sha512-eJopQrUk0WR7jViYDC29+Rp50xGvs4GtWOXBeqCoFMzutkkO3CZvHehA4JqnjfWMTSS8toqvRhCSOpOz62Wf9w==} + cpu: [arm64] + os: [darwin] + + '@oven/bun-darwin-x64-baseline@1.3.3': + resolution: {integrity: sha512-1ij4wQ9ECLFf1XFry+IFUN+28if40ozDqq6+QtuyOhIwraKzXOlAUbILhRMGvM3ED3yBex2mTwlKpA4Vja/V2g==} + cpu: [x64] + os: [darwin] + + '@oven/bun-darwin-x64@1.3.3': + resolution: {integrity: sha512-xGDePueVFrNgkS+iN0QdEFeRrx2MQ5hQ9ipRFu7N73rgoSSJsFlOKKt2uGZzunczedViIfjYl0ii0K4E9aZ0Ow==} + cpu: [x64] + os: [darwin] + + '@oven/bun-linux-aarch64-musl@1.3.3': + resolution: {integrity: sha512-XWQ3tV/gtZj0wn2AdSUq/tEOKWT4OY+Uww70EbODgrrq00jxuTfq5nnYP6rkLD0M/T5BHJdQRSfQYdIni9vldw==} + cpu: [arm64] + os: [linux] + + '@oven/bun-linux-aarch64@1.3.3': + resolution: {integrity: sha512-DabZ3Mt1XcJneWdEEug8l7bCPVvDBRBpjUIpNnRnMFWFnzr8KBEpMcaWTwYOghjXyJdhB4MPKb19MwqyQ+FHAw==} + cpu: [arm64] + os: [linux] + + '@oven/bun-linux-x64-baseline@1.3.3': + resolution: {integrity: sha512-IU8pxhIf845psOv55LqJyL+tSUc6HHMfs6FGhuJcAnyi92j+B1HjOhnFQh9MW4vjoo7do5F8AerXlvk59RGH2w==} + cpu: [x64] + os: [linux] + + '@oven/bun-linux-x64-musl-baseline@1.3.3': + resolution: {integrity: sha512-JoRTPdAXRkNYouUlJqEncMWUKn/3DiWP03A7weBbtbsKr787gcdNna2YeyQKCb1lIXE4v1k18RM3gaOpQobGIQ==} + cpu: [x64] + os: [linux] + + '@oven/bun-linux-x64-musl@1.3.3': + resolution: {integrity: sha512-xNSDRPn1yyObKteS8fyQogwsS4eCECswHHgaKM+/d4wy/omZQrXn8ZyGm/ZF9B73UfQytUfbhE7nEnrFq03f0w==} + cpu: [x64] + os: [linux] + + '@oven/bun-linux-x64@1.3.3': + resolution: {integrity: sha512-7eIARtKZKZDtah1aCpQUj/1/zT/zHRR063J6oAxZP9AuA547j5B9OM2D/vi/F4En7Gjk9FPjgPGTSYeqpQDzJw==} + cpu: [x64] + os: [linux] + + '@oven/bun-windows-x64-baseline@1.3.3': + resolution: {integrity: sha512-u5eZHKq6TPJSE282KyBOicGQ2trkFml0RoUfqkPOJVo7TXGrsGYYzdsugZRnVQY/WEmnxGtBy4T3PAaPqgQViA==} + cpu: [x64] + os: [win32] + + '@oven/bun-windows-x64@1.3.3': + resolution: {integrity: sha512-kWqa1LKvDdAIzyfHxo3zGz3HFWbFHDlrNK77hKjUN42ycikvZJ+SHSX76+1OW4G8wmLETX4Jj+4BM1y01DQRIQ==} + cpu: [x64] + os: [win32] + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -3620,9 +3704,20 @@ packages: react: optional: true + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + algoliasearch@5.29.0: resolution: {integrity: sha512-E2l6AlTWGznM2e7vEE6T6hzObvEyXukxMOlBmVlMyixZyK1umuO/CiVc6sDBbzVH0oEviCE5IfVY1oZBmccYPQ==} engines: {node: '>= 14.0.0'} @@ -3843,6 +3938,12 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + bun@1.3.3: + resolution: {integrity: sha512-2hJ4ocTZ634/Ptph4lysvO+LbbRZq8fzRvMwX0/CqaLBxrF2UB5D1LdMB8qGcdtCer4/VR9Bx5ORub0yn+yzmw==} + cpu: [arm64, x64] + os: [darwin, linux, win32] + hasBin: true + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -4884,6 +4985,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -5788,6 +5892,9 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} @@ -6864,6 +6971,11 @@ packages: peerDependencies: react: ^19.1.0 + react-dom@19.2.0: + resolution: {integrity: sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==} + peerDependencies: + react: ^19.2.0 + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -6948,6 +7060,10 @@ packages: resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} engines: {node: '>=0.10.0'} + react@19.2.0: + resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==} + engines: {node: '>=0.10.0'} + read-package-up@11.0.0: resolution: {integrity: sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==} engines: {node: '>=18'} @@ -7117,6 +7233,9 @@ packages: scheduler@0.26.0: resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + search-insights@2.17.3: resolution: {integrity: sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==} @@ -7708,6 +7827,11 @@ packages: engines: {node: '>=14.17'} hasBin: true + typescript@5.7.2: + resolution: {integrity: sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==} + engines: {node: '>=14.17'} + hasBin: true + typescript@5.8.3: resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} engines: {node: '>=14.17'} @@ -8828,9 +8952,9 @@ snapshots: - react-dom - search-insights - '@docsearch/js@3.8.2(@algolia/client-search@5.29.0)(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(search-insights@2.17.3)': + '@docsearch/js@3.8.2(@algolia/client-search@5.29.0)(@types/react@19.1.8)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(search-insights@2.17.3)': dependencies: - '@docsearch/react': 3.8.2(@algolia/client-search@5.29.0)(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(search-insights@2.17.3) + '@docsearch/react': 3.8.2(@algolia/client-search@5.29.0)(@types/react@19.1.8)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(search-insights@2.17.3) preact: 10.26.9 transitivePeerDependencies: - '@algolia/client-search' @@ -8853,7 +8977,7 @@ snapshots: transitivePeerDependencies: - '@algolia/client-search' - '@docsearch/react@3.8.2(@algolia/client-search@5.29.0)(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(search-insights@2.17.3)': + '@docsearch/react@3.8.2(@algolia/client-search@5.29.0)(@types/react@19.1.8)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(search-insights@2.17.3)': dependencies: '@algolia/autocomplete-core': 1.17.7(@algolia/client-search@5.29.0)(algoliasearch@5.29.0)(search-insights@2.17.3) '@algolia/autocomplete-preset-algolia': 1.17.7(@algolia/client-search@5.29.0)(algoliasearch@5.29.0) @@ -8861,8 +8985,8 @@ snapshots: algoliasearch: 5.29.0 optionalDependencies: '@types/react': 19.1.8 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) search-insights: 2.17.3 transitivePeerDependencies: - '@algolia/client-search' @@ -9692,6 +9816,18 @@ snapshots: '@mjackson/node-fetch-server@0.6.1': {} + '@modelcontextprotocol/ext-apps@git+https://git@github.com:modelcontextprotocol/ext-apps.git#f53643bac71cad4997cc92b0ac955194bb298ff8': + dependencies: + '@modelcontextprotocol/sdk': 1.22.0 + bun: 1.3.3 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + typescript: 5.7.2 + zod: 3.25.67 + transitivePeerDependencies: + - '@cfworker/json-schema' + - supports-color + '@modelcontextprotocol/sdk@1.13.1': dependencies: ajv: 6.12.6 @@ -9725,6 +9861,24 @@ snapshots: transitivePeerDependencies: - supports-color + '@modelcontextprotocol/sdk@1.22.0': + dependencies: + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + content-type: 1.0.5 + cors: 2.8.5 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.2 + express: 5.1.0 + express-rate-limit: 7.5.1(express@5.1.0) + pkce-challenge: 5.0.0 + raw-body: 3.0.0 + zod: 3.25.67 + zod-to-json-schema: 3.24.5(zod@3.25.67) + transitivePeerDependencies: + - supports-color + '@napi-rs/wasm-runtime@0.2.11': dependencies: '@emnapi/core': 1.4.3 @@ -9893,6 +10047,39 @@ snapshots: '@opentelemetry/api@1.9.0': {} + '@oven/bun-darwin-aarch64@1.3.3': + optional: true + + '@oven/bun-darwin-x64-baseline@1.3.3': + optional: true + + '@oven/bun-darwin-x64@1.3.3': + optional: true + + '@oven/bun-linux-aarch64-musl@1.3.3': + optional: true + + '@oven/bun-linux-aarch64@1.3.3': + optional: true + + '@oven/bun-linux-x64-baseline@1.3.3': + optional: true + + '@oven/bun-linux-x64-musl-baseline@1.3.3': + optional: true + + '@oven/bun-linux-x64-musl@1.3.3': + optional: true + + '@oven/bun-linux-x64@1.3.3': + optional: true + + '@oven/bun-windows-x64-baseline@1.3.3': + optional: true + + '@oven/bun-windows-x64@1.3.3': + optional: true + '@pkgjs/parseargs@0.11.0': optional: true @@ -10720,13 +10907,13 @@ snapshots: transitivePeerDependencies: - '@types/react' - '@testing-library/react@14.3.1(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@testing-library/react@14.3.1(@types/react@19.1.8)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@babel/runtime': 7.27.6 '@testing-library/dom': 9.3.4 '@types/react-dom': 18.3.7(@types/react@19.1.8) - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) transitivePeerDependencies: - '@types/react' @@ -11702,6 +11889,10 @@ snapshots: optionalDependencies: react: 19.1.0 + ajv-formats@3.0.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -11709,6 +11900,13 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + algoliasearch@5.29.0: dependencies: '@algolia/client-abtesting': 5.29.0 @@ -12003,6 +12201,20 @@ snapshots: buffer-from@1.1.2: {} + bun@1.3.3: + optionalDependencies: + '@oven/bun-darwin-aarch64': 1.3.3 + '@oven/bun-darwin-x64': 1.3.3 + '@oven/bun-darwin-x64-baseline': 1.3.3 + '@oven/bun-linux-aarch64': 1.3.3 + '@oven/bun-linux-aarch64-musl': 1.3.3 + '@oven/bun-linux-x64': 1.3.3 + '@oven/bun-linux-x64-baseline': 1.3.3 + '@oven/bun-linux-x64-musl': 1.3.3 + '@oven/bun-linux-x64-musl-baseline': 1.3.3 + '@oven/bun-windows-x64': 1.3.3 + '@oven/bun-windows-x64-baseline': 1.3.3 + bytes@3.1.2: {} c8@8.0.1: @@ -13349,6 +13561,8 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-uri@3.1.0: {} + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -14550,6 +14764,8 @@ snapshots: json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: {} + json-schema@0.4.0: {} json-stable-stringify-without-jsonify@1.0.1: {} @@ -15440,6 +15656,11 @@ snapshots: react: 19.1.0 scheduler: 0.26.0 + react-dom@19.2.0(react@19.2.0): + dependencies: + react: 19.2.0 + scheduler: 0.27.0 + react-is@16.13.1: {} react-is@17.0.2: {} @@ -15515,6 +15736,8 @@ snapshots: react@19.1.0: {} + react@19.2.0: {} + read-package-up@11.0.0: dependencies: find-up-simple: 1.0.1 @@ -15739,6 +15962,8 @@ snapshots: scheduler@0.26.0: {} + scheduler@0.27.0: {} + search-insights@2.17.3: {} secure-json-parse@2.7.0: {} @@ -16442,6 +16667,8 @@ snapshots: typescript@5.4.2: {} + typescript@5.7.2: {} + typescript@5.8.3: {} ufo@1.6.1: {} @@ -16867,10 +17094,10 @@ snapshots: - typescript - universal-cookie - vitepress@1.6.3(@algolia/client-search@5.29.0)(@types/node@22.15.33)(@types/react@19.1.8)(lightningcss@1.30.1)(postcss@8.5.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(search-insights@2.17.3)(typescript@5.8.3): + vitepress@1.6.3(@algolia/client-search@5.29.0)(@types/node@22.15.33)(@types/react@19.1.8)(lightningcss@1.30.1)(postcss@8.5.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(search-insights@2.17.3)(typescript@5.8.3): dependencies: '@docsearch/css': 3.8.2 - '@docsearch/js': 3.8.2(@algolia/client-search@5.29.0)(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(search-insights@2.17.3) + '@docsearch/js': 3.8.2(@algolia/client-search@5.29.0)(@types/react@19.1.8)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(search-insights@2.17.3) '@iconify-json/simple-icons': 1.2.40 '@shikijs/core': 2.5.0 '@shikijs/transformers': 2.5.0 diff --git a/sdks/typescript/client/package.json b/sdks/typescript/client/package.json index 27fcde9d..788a6908 100644 --- a/sdks/typescript/client/package.json +++ b/sdks/typescript/client/package.json @@ -19,11 +19,14 @@ "dist" ], "dependencies": { + "@modelcontextprotocol/ext-apps": "git+https://github.com/modelcontextprotocol/ext-apps.git", "@modelcontextprotocol/sdk": "*", + "@mcp-ui/shared": "workspace:*", "@quilted/threads": "^3.1.3", "@r2wc/react-to-web-component": "^2.0.4", "@remote-dom/core": "^1.8.0", - "@remote-dom/react": "^1.2.2" + "@remote-dom/react": "^1.2.2", + "zod": "^3.23.8" }, "devDependencies": { "@testing-library/jest-dom": "^6.0.0", diff --git a/sdks/typescript/client/src/components/UITemplatedToolCallRenderer.tsx b/sdks/typescript/client/src/components/UITemplatedToolCallRenderer.tsx new file mode 100644 index 00000000..ada57b77 --- /dev/null +++ b/sdks/typescript/client/src/components/UITemplatedToolCallRenderer.tsx @@ -0,0 +1,484 @@ +import { useEffect, useRef, useState } from "react"; +import { + McpUiMessageRequestSchema, + McpUiOpenLinkRequestSchema, + McpUiSizeChangeNotificationSchema, + McpUiSandboxProxyReadyNotificationSchema, + PostMessageTransport, +} from "@modelcontextprotocol/ext-apps"; +import { + AppBridge, +} from "@modelcontextprotocol/ext-apps/app-bridge"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { + CallToolResult, + LoggingMessageNotificationSchema, + Tool, +} from "@modelcontextprotocol/sdk/types.js"; +import { UIActionResult } from ".."; + +/** + * Props for the UITemplatedToolCallRenderer component. + */ +export interface UITemplatedToolCallRendererProps { + /** URL to the sandbox proxy HTML that will host the tool UI iframe */ + sandboxProxyUrl: URL; + + /** MCP client connected to the server providing the tool */ + client: Client; + + /** Name of the MCP tool to render UI for */ + toolName: string; + + /** Optional pre-fetched resource URI. If not provided, will be fetched via getToolUiResourceUri() */ + toolResourceUri?: string; + + /** Optional input arguments to pass to the tool UI once it's ready */ + toolInput?: Record; + + /** Optional result from tool execution to pass to the tool UI once it's ready */ + toolResult?: CallToolResult; + + /** Callback invoked when the tool UI requests an action (intent, link, prompt, notify) */ + onUIAction?: (result: UIActionResult) => Promise; + + /** Callback invoked when an error occurs during setup or message handling */ + onError?: (error: Error) => void; +} + +/** + * React component that renders an MCP tool's custom UI in a sandboxed iframe. + * + * This component manages the complete lifecycle of an MCP-UI tool: + * 1. Creates a sandboxed iframe with the proxy HTML + * 2. Establishes MCP communication channel between host and iframe + * 3. Fetches and loads the tool's UI resource (HTML) + * 4. Sends tool inputs and results to the UI when ready + * 5. Handles UI actions (intents, link opening, prompts, notifications) + * 6. Automatically resizes iframe based on content size changes + * + * @example + * ```tsx + * { + * if (action.type === 'intent') { + * // Handle intent request from UI + * console.log('Intent:', action.payload.intent); + * } + * }} + * onError={(error) => console.error('UI Error:', error)} + * /> + * ``` + * + * **Architecture:** + * - Host (this component) ↔ Sandbox Proxy (iframe) ↔ Tool UI (nested iframe) + * - Communication uses MCP protocol over postMessage + * - Sandbox proxy provides CSP isolation for untrusted tool UIs + * - Standard MCP initialization flow determines when UI is ready + * + * **Lifecycle:** + * 1. `setupSandboxProxyIframe()` creates iframe and waits for proxy ready + * 2. Component creates `McpUiProxyServer` instance + * 3. Registers all handlers (BEFORE connecting to avoid race conditions) + * 4. Connects proxy to iframe via `MessageTransport` + * 5. MCP initialization completes → `onClientReady` callback fires + * 6. Fetches tool UI resource and sends to sandbox proxy + * 7. Sends tool inputs/results when iframe signals ready + * + * @param props - Component props + * @returns React element containing the sandboxed tool UI iframe + */ +export const UITemplatedToolCallRenderer = ( + props: UITemplatedToolCallRendererProps, +) => { + const { + client, + sandboxProxyUrl, + toolName, + toolResourceUri, + // usesOpenAiAppsSdk, + toolInput, + toolResult, + onUIAction, + onError, + } = props; + + // State + const [appBridge, setAppBridge] = useState(null); + const [iframeReady, setIframeReady] = useState(false); + const [error, setError] = useState(null); + const containerRef = useRef(null); + const iframeRef = useRef(null); + + // Use refs for callbacks to avoid effect re-runs when they change + const onUIActionRef = useRef(onUIAction); + const onErrorRef = useRef(onError); + + useEffect(() => { + onUIActionRef.current = onUIAction; + onErrorRef.current = onError; + }); + + // Effect 1: Setup sandbox proxy iframe + useEffect(() => { + let mounted = true; + + const setup = async () => { + try { + // Step 1: Create iframe and wait for sandbox proxy ready + const { iframe, onReady } = + await setupSandboxProxyIframe(sandboxProxyUrl); + + if (!mounted) return; + + // Append iframe to DOM + iframeRef.current = iframe; + if (containerRef.current) { + containerRef.current.appendChild(iframe); + } + + // Wait for sandbox proxy HTML to signal it's ready + await onReady; + + if (!mounted) return; + + // Step 2: Create proxy server instance + const serverCapabilities = client.getServerCapabilities(); + const appBridge = new AppBridge( + client, + { + name: "Example MCP UI Host", + version: "1.0.0", + }, + { + openLinks: {}, + serverTools: serverCapabilities?.tools, + serverResources: serverCapabilities?.resources, + }, + ); + + // Step 3: Register ALL handlers BEFORE connecting (critical for avoiding race conditions) + + // Hook into the standard MCP initialization to know when the inner iframe is ready + appBridge.oninitialized = () => { + if (!mounted) return; + console.log("[Host] Inner iframe MCP client initialized"); + setIframeReady(true); + }; + + // Register MCP-UI specific request handlers + + appBridge.setRequestHandler(McpUiOpenLinkRequestSchema, async (req) => { + try { + await onUIActionRef.current?.({ + type: "link", + payload: { url: req.params.url }, + }); + return { isError: false }; + } catch (e) { + console.error("[Host] Open link handler error:", e); + const error = e instanceof Error ? e : new Error(String(e)); + onErrorRef.current?.(error); + return { isError: true }; + } + }); + + appBridge.setRequestHandler(McpUiMessageRequestSchema, async (req) => { + try { + await onUIActionRef.current?.({ + type: "prompt", + payload: { + prompt: req.params.content + .map((c: any) => (c.type === "text" ? c.text : "")) + .join("\n"), + }, + }); + return { isError: false }; + } catch (e) { + console.error("[Host] Message handler error:", e); + const error = e instanceof Error ? e : new Error(String(e)); + onErrorRef.current?.(error); + return { isError: true }; + } + }); + + appBridge.setNotificationHandler( + McpUiSizeChangeNotificationSchema, + async (notif: any) => { + const { width, height } = notif.params; + if (iframeRef.current) { + if (width !== undefined) { + iframeRef.current.style.width = `${width}px`; + } + if (height !== undefined) { + iframeRef.current.style.height = `${height}px`; + } + } + }, + ); + + appBridge.setNotificationHandler( + LoggingMessageNotificationSchema, + async (notif: any) => { + onUIAction?.({ + type: "notify", + payload: { + message: notif.params.message, + }, + }); + }, + ); + + // Step 4: NOW connect (triggers MCP initialization handshake) + // IMPORTANT: Pass iframe.contentWindow as BOTH target and source to ensure + // this proxy only responds to messages from its specific iframe + await appBridge.connect( + new PostMessageTransport( + iframe.contentWindow!, + iframe.contentWindow!, + ), + ); + + if (!mounted) return; + + // Step 5: Store proxy in state + setAppBridge(appBridge); + } catch (err) { + console.error("[UITemplatedToolCallRenderer] Error:", err); + if (!mounted) return; + const error = err instanceof Error ? err : new Error(String(err)); + setError(error); + onErrorRef.current?.(error); + } + }; + + setup(); + + return () => { + mounted = false; + // Cleanup: remove iframe from DOM + if ( + iframeRef.current && + containerRef.current?.contains(iframeRef.current) + ) { + containerRef.current.removeChild(iframeRef.current); + } + }; + }, [client, sandboxProxyUrl]); + + // Effect 2: Fetch and send UI resource + useEffect(() => { + if (!appBridge) return; + + let mounted = true; + + const fetchAndSendResource = async () => { + try { + // Get the resource URI (use prop if provided, otherwise fetch) + let resourceInfo: { uri: string }; + + if (toolResourceUri) { + // When URI is provided directly, assume it's NOT OpenAI Apps SDK format + resourceInfo = { + uri: toolResourceUri, + // usesOpenAiAppsSdk: usesOpenAiAppsSdk ?? false, + }; + console.log( + `[Host] Using provided resource URI: ${resourceInfo.uri}`, + ); + } else { + console.log(`[Host] Fetching resource URI for tool: ${toolName}`); + const info = await getToolUiResourceUri(client, toolName); + if (!info) { + throw new Error( + `Tool ${toolName} has no UI resource (no ui/resourceUri or openai/outputTemplate in tool._meta)`, + ); + } + resourceInfo = info; + console.log(`[Host] Got resource URI: ${resourceInfo.uri}`); + } + + if (!resourceInfo.uri) { + throw new Error(`Tool ${toolName}: URI is undefined or empty`); + } + + if (!mounted) return; + + // Read the HTML content + console.log(`[Host] Reading resource HTML from: ${resourceInfo.uri}`); + const html = await readToolUiResourceHtml(client, { + uri: resourceInfo.uri, + }); + + if (!mounted) return; + + // Send the resource to the sandbox proxy + console.log("[Host] Sending sandbox resource ready"); + await appBridge.sendSandboxResourceReady({ html }); + } catch (err) { + if (!mounted) return; + const error = err instanceof Error ? err : new Error(String(err)); + setError(error); + onErrorRef.current?.(error); + } + }; + + fetchAndSendResource(); + + return () => { + mounted = false; + }; + }, [appBridge, toolName, toolResourceUri, client]); + + // Effect 3: Send tool input when ready + useEffect(() => { + if (appBridge && iframeReady && toolInput) { + console.log("[Host] Sending tool input:", toolInput); + appBridge.sendToolInput({ arguments: toolInput }); + } + }, [appBridge, iframeReady, toolInput]); + + // Effect 4: Send tool result when ready + useEffect(() => { + if (appBridge && iframeReady && toolResult) { + console.log("[Host] Sending tool result:", toolResult); + appBridge.sendToolResult(toolResult); + } + }, [appBridge, iframeReady, toolResult]); + + // Render + return ( +
+ {error && ( +
+ Error: {error.message} +
+ )} +
+ ); +}; + + +const MCP_UI_RESOURCE_META_KEY = "ui/resourceUri"; + +async function setupSandboxProxyIframe(sandboxProxyUrl: URL): Promise<{ + iframe: HTMLIFrameElement; + onReady: Promise; +}> { + const iframe = document.createElement("iframe"); + iframe.style.width = "100%"; + iframe.style.height = "100px"; + iframe.style.border = "none"; + iframe.style.backgroundColor = "transparent"; + iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms"); + + const onReady = new Promise((resolve, _reject) => { + const initialListener = async (event: MessageEvent) => { + if (event.source === iframe.contentWindow) { + if ( + event.data && + event.data.method === + McpUiSandboxProxyReadyNotificationSchema.shape.method._def.value + ) { + window.removeEventListener("message", initialListener); + resolve(); + } + } + }; + window.addEventListener("message", initialListener); + }); + + iframe.src = sandboxProxyUrl.href; + + return { iframe, onReady }; +} + +type ToolUiResourceInfo = { + uri: string; +}; + +async function getToolUiResourceUri( + client: Client, + toolName: string, +): Promise { + let tool: Tool | undefined; + let cursor: string | undefined = undefined; + do { + const toolsResult = await client.listTools({ cursor }); + tool = toolsResult.tools.find((t) => t.name === toolName); + cursor = toolsResult.nextCursor; + } while (!tool && cursor); + if (!tool) { + throw new Error(`tool ${toolName} not found`); + } + if (!tool._meta) { + return null; + } + + let uri: string; + if (MCP_UI_RESOURCE_META_KEY in tool._meta) { + uri = String(tool._meta[MCP_UI_RESOURCE_META_KEY]); + } else { + return null; + } + if (!uri.startsWith("ui://")) { + throw new Error( + `tool ${toolName} has unsupported output template URI: ${uri}`, + ); + } + return { uri }; +} + +async function readToolUiResourceHtml( + client: Client, + opts: { + uri: string; + }, +): Promise { + const resource = await client.readResource({ uri: opts.uri }); + + if (!resource) { + throw new Error("UI resource not found: " + opts.uri); + } + if (resource.contents.length !== 1) { + throw new Error( + "Unsupported UI resource content length: " + resource.contents.length, + ); + } + const content = resource.contents[0]; + let html: string; + const isHtml = (t?: string) => + t === "text/html" || t === "text/html+skybridge"; + + if ( + "text" in content && + typeof content.text === "string" && + isHtml(content.mimeType) + ) { + html = content.text; + } else if ( + "blob" in content && + typeof content.blob === "string" && + isHtml(content.mimeType) + ) { + html = atob(content.blob); + } else { + throw new Error( + "Unsupported UI resource content format: " + JSON.stringify(content), + ); + } + + return html; +} diff --git a/sdks/typescript/client/src/index.ts b/sdks/typescript/client/src/index.ts index 49734e7d..8ae092fb 100644 --- a/sdks/typescript/client/src/index.ts +++ b/sdks/typescript/client/src/index.ts @@ -2,6 +2,9 @@ export { UIResourceRenderer } from './components/UIResourceRenderer'; export { getUIResourceMetadata, getResourceMetadata } from './utils/metadataUtils'; export { isUIResource } from './utils/isUIResource'; +// MCP-UI Templated Tool Call Renderer +export { UITemplatedToolCallRenderer, type UITemplatedToolCallRendererProps } from './components/UITemplatedToolCallRenderer'; + // The types needed to create a custom component library export type { ComponentLibrary, From 73c2c0d509a78331164480a65d8bd77abe75d182 Mon Sep 17 00:00:00 2001 From: ochafik Date: Sat, 22 Nov 2025 20:34:59 +0000 Subject: [PATCH 02/29] Upgrade MCP SDK to 1.22.0 (many (Embedded)Resource type fixes) --- examples/server/package-lock.json | 2 +- examples/server/package.json | 2 +- .../src/components/HTMLResourceRenderer.tsx | 4 ++-- .../src/components/RemoteDOMResourceRenderer.tsx | 4 ++-- .../client/src/components/UIResourceRenderer.tsx | 4 ++-- .../src/components/UIResourceRendererWC.tsx | 8 ++++---- .../__tests__/HTMLResourceRenderer.test.tsx | 4 ++-- .../__tests__/UIResourceRenderer.test.tsx | 2 +- .../UIResourceRenderer.unmocked.test.tsx | 6 +++--- .../client/src/utils/processResource.ts | 16 ++++++++-------- 10 files changed, 26 insertions(+), 26 deletions(-) diff --git a/examples/server/package-lock.json b/examples/server/package-lock.json index 4b8192c3..3f2fe99c 100644 --- a/examples/server/package-lock.json +++ b/examples/server/package-lock.json @@ -8,7 +8,7 @@ "name": "remote-mcp-server-authless", "version": "0.0.0", "dependencies": { - "@modelcontextprotocol/sdk": "^1.11.1", + "@modelcontextprotocol/sdk": "^1.22.0", "agents": "^0.0.80", "zod": "^3.24.4" }, diff --git a/examples/server/package.json b/examples/server/package.json index 052b8eac..bff35150 100644 --- a/examples/server/package.json +++ b/examples/server/package.json @@ -13,7 +13,7 @@ "packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977", "dependencies": { "@mcp-ui/server": "^5.2.0", - "@modelcontextprotocol/sdk": "^1.11.1", + "@modelcontextprotocol/sdk": "^1.22.0", "@vis.gl/react-google-maps": "^1.5.2", "agents": "^0.0.80", "isbot": "^5.1.27", diff --git a/sdks/typescript/client/src/components/HTMLResourceRenderer.tsx b/sdks/typescript/client/src/components/HTMLResourceRenderer.tsx index eef08fc3..17090825 100644 --- a/sdks/typescript/client/src/components/HTMLResourceRenderer.tsx +++ b/sdks/typescript/client/src/components/HTMLResourceRenderer.tsx @@ -1,11 +1,11 @@ import React, { useCallback, useEffect, useImperativeHandle, useMemo, useRef } from 'react'; -import type { Resource } from '@modelcontextprotocol/sdk/types.js'; +import type { EmbeddedResource } from '@modelcontextprotocol/sdk/types.js'; import { UIActionResult, UIMetadataKey } from '../types'; import { processHTMLResource } from '../utils/processResource'; import { getUIResourceMetadata } from '../utils/metadataUtils'; export type HTMLResourceRendererProps = { - resource: Partial; + resource: Partial; onUIAction?: (result: UIActionResult) => Promise; style?: React.CSSProperties; proxy?: string; diff --git a/sdks/typescript/client/src/components/RemoteDOMResourceRenderer.tsx b/sdks/typescript/client/src/components/RemoteDOMResourceRenderer.tsx index 41e5a88a..aeb9cce5 100644 --- a/sdks/typescript/client/src/components/RemoteDOMResourceRenderer.tsx +++ b/sdks/typescript/client/src/components/RemoteDOMResourceRenderer.tsx @@ -5,7 +5,7 @@ import { RemoteRootRenderer, RemoteReceiver, } from '@remote-dom/react/host'; -import type { Resource } from '@modelcontextprotocol/sdk/types.js'; +import type { EmbeddedResource } from '@modelcontextprotocol/sdk/types.js'; import { IFRAME_SRC_DOC } from '../remote-dom/iframe-bundle'; import { ThreadIframe } from '@quilted/threads'; import type { @@ -20,7 +20,7 @@ import { RemoteDOMRenderer } from './RemoteDOMRenderer'; import { processRemoteDOMResource } from '../utils/processResource'; export type RemoteDOMResourceProps = { - resource: Partial; + resource: Partial; library?: ComponentLibrary; remoteElements?: RemoteElementConfiguration[]; onUIAction?: (result: UIActionResult) => Promise; diff --git a/sdks/typescript/client/src/components/UIResourceRenderer.tsx b/sdks/typescript/client/src/components/UIResourceRenderer.tsx index c92549ce..b136e637 100644 --- a/sdks/typescript/client/src/components/UIResourceRenderer.tsx +++ b/sdks/typescript/client/src/components/UIResourceRenderer.tsx @@ -5,7 +5,7 @@ import { RemoteDOMResourceProps, RemoteDOMResourceRenderer } from './RemoteDOMRe import { basicComponentLibrary } from '../remote-dom/component-libraries/basic'; export type UIResourceRendererProps = { - resource: Partial; + resource: Partial; onUIAction?: (result: UIActionResult) => Promise; supportedContentTypes?: ResourceContentType[]; htmlProps?: Omit; @@ -13,7 +13,7 @@ export type UIResourceRendererProps = { }; function getContentType( - resource: Partial, + resource: Partial, ): ResourceContentType | undefined { if (resource.contentType) { return resource.contentType as ResourceContentType; diff --git a/sdks/typescript/client/src/components/UIResourceRendererWC.tsx b/sdks/typescript/client/src/components/UIResourceRendererWC.tsx index b69ae0d3..33740688 100644 --- a/sdks/typescript/client/src/components/UIResourceRendererWC.tsx +++ b/sdks/typescript/client/src/components/UIResourceRendererWC.tsx @@ -2,11 +2,11 @@ import r2wc from '@r2wc/react-to-web-component'; import { UIResourceRenderer, type UIResourceRendererProps } from './UIResourceRenderer'; import { FC, useCallback, useRef } from 'react'; import { UIActionResult } from '../types'; -import { Resource } from '@modelcontextprotocol/sdk/types.js'; +import { EmbeddedResource } from '@modelcontextprotocol/sdk/types.js'; type UIResourceRendererWCProps = Omit & { - resource?: Resource | string; + resource?: EmbeddedResource['resource'] | string; }; function normalizeJsonProp(prop: unknown): Record | undefined { @@ -31,7 +31,7 @@ export const UIResourceRendererWCWrapper: FC = (props remoteDomProps: rawRemoteDomProps, } = props; - const resource = normalizeJsonProp(rawResource); + const resource = normalizeJsonProp(rawResource) as Partial | undefined; const supportedContentTypes = normalizeJsonProp(rawSupportedContentTypes); const htmlProps = normalizeJsonProp(rawHtmlProps); const remoteDomProps = normalizeJsonProp(rawRemoteDomProps); @@ -56,7 +56,7 @@ export const UIResourceRendererWCWrapper: FC = (props return (
{ vi.clearAllMocks(); }); - const mockResourceBaseForUIActionTests: Partial = { + const mockResourceBaseForUIActionTests: Partial = { mimeType: 'text/html', text: '

Test Content

', }; diff --git a/sdks/typescript/client/src/components/__tests__/UIResourceRenderer.test.tsx b/sdks/typescript/client/src/components/__tests__/UIResourceRenderer.test.tsx index 248535ad..6567d433 100644 --- a/sdks/typescript/client/src/components/__tests__/UIResourceRenderer.test.tsx +++ b/sdks/typescript/client/src/components/__tests__/UIResourceRenderer.test.tsx @@ -17,7 +17,7 @@ vi.mock('../RemoteDOMResourceRenderer', () => ({ describe('', () => { const baseResource = { uri: 'ui://test-resource', - content: 'test content', + text: 'test content', }; afterEach(() => { diff --git a/sdks/typescript/client/src/components/__tests__/UIResourceRenderer.unmocked.test.tsx b/sdks/typescript/client/src/components/__tests__/UIResourceRenderer.unmocked.test.tsx index 7484d29c..d27d391d 100644 --- a/sdks/typescript/client/src/components/__tests__/UIResourceRenderer.unmocked.test.tsx +++ b/sdks/typescript/client/src/components/__tests__/UIResourceRenderer.unmocked.test.tsx @@ -1,12 +1,12 @@ import { fireEvent, render } from '@testing-library/react'; import '@testing-library/jest-dom'; import React from 'react'; -import { Resource } from '@modelcontextprotocol/sdk/types.js'; +import { EmbeddedResource } from '@modelcontextprotocol/sdk/types.js'; import { UIResourceRenderer } from '../UIResourceRenderer'; import { UI_METADATA_PREFIX } from '../../types'; describe('UIResourceRenderer', () => { - const testResource: Partial = { + const testResource: Partial = { mimeType: 'text/html', text: `

Test Content