diff --git a/package.json b/package.json
index 860cff737..59938d2e3 100644
--- a/package.json
+++ b/package.json
@@ -87,22 +87,21 @@
"license": "Apache-2.0",
"devDependencies": {
"@ai-sdk/azure": "^2.0.53",
- "@emotion/css": "^11.13.5",
"@ai-sdk/google": "^2.0.23",
"@ai-sdk/mcp": "^0.0.8",
"@ai-sdk/openai": "^2.0.52",
+ "@emotion/css": "^11.13.5",
"@eslint/js": "^9.34.0",
"@leafygreen-ui/table": "^15.2.2",
"@modelcontextprotocol/inspector": "^0.17.1",
"@mongodb-js/oidc-mock-provider": "^0.12.0",
"@redocly/cli": "^2.0.8",
+ "@testing-library/react": "^16.3.1",
"@types/express": "^5.0.3",
"@types/node": "^24.5.2",
"@types/proper-lockfile": "^4.1.4",
"@types/react": "^18.3.0",
"@types/react-dom": "^19.2.3",
- "react": "^18.3.0",
- "react-dom": "^18.3.0",
"@types/semver": "^7.7.0",
"@types/yargs-parser": "^21.0.3",
"@typescript-eslint/parser": "^8.44.0",
@@ -116,6 +115,7 @@
"eslint-plugin-prettier": "^5.5.4",
"globals": "^16.3.0",
"husky": "^9.1.7",
+ "jsdom": "^27.3.0",
"knip": "^5.63.1",
"mongodb": "^6.21.0",
"mongodb-runner": "^6.2.0",
@@ -123,6 +123,8 @@
"openapi-typescript": "^7.9.1",
"prettier": "^3.6.2",
"proper-lockfile": "^4.1.2",
+ "react": "^18.3.0",
+ "react-dom": "^18.3.0",
"semver": "^7.7.2",
"simple-git": "^3.28.0",
"testcontainers": "^11.7.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c5c59d8f4..f037dbfc7 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -106,6 +106,9 @@ importers:
'@redocly/cli':
specifier: ^2.0.8
version: 2.12.5(@opentelemetry/api@1.9.0)(ajv@8.17.1)(core-js@3.47.0)
+ '@testing-library/react':
+ specifier: ^16.3.1
+ version: 16.3.1(@testing-library/dom@9.3.1)(@types/react-dom@19.2.3(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@types/express':
specifier: ^5.0.3
version: 5.0.6
@@ -135,10 +138,10 @@ importers:
version: 5.1.1(vite@5.4.21(@types/node@24.10.1))
'@vitest/coverage-v8':
specifier: ^3.2.4
- version: 3.2.4(vitest@3.2.4(@types/node@24.10.1)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))
+ version: 3.2.4(vitest@3.2.4(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.3.0)(tsx@4.21.0)(yaml@2.8.2))
'@vitest/eslint-plugin':
specifier: ^1.3.4
- version: 1.4.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)(vitest@3.2.4(@types/node@24.10.1)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))
+ version: 1.4.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)(vitest@3.2.4(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.3.0)(tsx@4.21.0)(yaml@2.8.2))
concurrently:
specifier: ^9.2.1
version: 9.2.1
@@ -160,6 +163,9 @@ importers:
husky:
specifier: ^9.1.7
version: 9.1.7
+ jsdom:
+ specifier: ^27.3.0
+ version: 27.3.0
knip:
specifier: ^5.63.1
version: 5.70.1(@types/node@24.10.1)(typescript@5.9.3)
@@ -219,7 +225,7 @@ importers:
version: 2.3.0(rollup@4.53.3)(vite@5.4.21(@types/node@24.10.1))
vitest:
specifier: ^3.2.4
- version: 3.2.4(@types/node@24.10.1)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)
+ version: 3.2.4(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.3.0)(tsx@4.21.0)(yaml@2.8.2)
optionalDependencies:
'@mongodb-js/atlas-local':
specifier: ^1.1.0
@@ -230,6 +236,9 @@ importers:
packages:
+ '@acemir/cssom@0.9.29':
+ resolution: {integrity: sha512-G90x0VW+9nW4dFajtjCoT+NM0scAfH9Mb08IcjgFHYbfiL/lU04dTF9JuVOi3/OH+DJCQdcIseSXkdCB9Ky6JA==}
+
'@ai-sdk/azure@2.0.71':
resolution: {integrity: sha512-AMwgXMHcs9uJoM+TaR6mPlmyUlP4JRcPV27Evou57StYWO9kUu/ygU2yjPMFwcaouu/Nl9mQki59mFNzF+03qQ==}
engines: {node: '>=18'}
@@ -280,6 +289,15 @@ packages:
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
engines: {node: '>=6.0.0'}
+ '@asamuzakjp/css-color@4.1.1':
+ resolution: {integrity: sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ==}
+
+ '@asamuzakjp/dom-selector@6.7.6':
+ resolution: {integrity: sha512-hBaJER6A9MpdG3WgdlOolHmbOYvSk46y7IQN/1+iqiCuUu6iWdQrs9DGKF8ocqsEqWujWf/V7b7vaDgiUmIvUg==}
+
+ '@asamuzakjp/nwsapi@2.3.9':
+ resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==}
+
'@babel/code-frame@7.27.1':
resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
engines: {node: '>=6.9.0'}
@@ -378,6 +396,38 @@ packages:
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'}
+ '@csstools/color-helpers@5.1.0':
+ resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==}
+ engines: {node: '>=18'}
+
+ '@csstools/css-calc@2.1.4':
+ resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@csstools/css-parser-algorithms': ^3.0.5
+ '@csstools/css-tokenizer': ^3.0.4
+
+ '@csstools/css-color-parser@3.1.0':
+ resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@csstools/css-parser-algorithms': ^3.0.5
+ '@csstools/css-tokenizer': ^3.0.4
+
+ '@csstools/css-parser-algorithms@3.0.5':
+ resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@csstools/css-tokenizer': ^3.0.4
+
+ '@csstools/css-syntax-patches-for-csstree@1.0.22':
+ resolution: {integrity: sha512-qBcx6zYlhleiFfdtzkRgwNC7VVoAwfK76Vmsw5t+PbvtdknO9StgRk7ROvq9so1iqbdW4uLIDAsXRsTfUrIoOw==}
+ engines: {node: '>=18'}
+
+ '@csstools/css-tokenizer@3.0.4':
+ resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==}
+ engines: {node: '>=18'}
+
'@emnapi/core@1.7.1':
resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==}
@@ -2037,6 +2087,21 @@ packages:
resolution: {integrity: sha512-0DGPd9AR3+iDTjGoMpxIkAsUihHZ3Ai6CneU6bRRrffXMgzCdlNk43jTrD2/5LT6CBb3MWTP8v510JzYtahD2w==}
engines: {node: '>=14'}
+ '@testing-library/react@16.3.1':
+ resolution: {integrity: sha512-gr4KtAWqIOQoucWYD/f6ki+j5chXfcPc74Col/6poTyqTmn7zRmodWahWRCp8tYd+GMqBonw6hstNzqjbs6gjw==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@testing-library/dom': ^10.0.0
+ '@types/react': ^18.0.0 || ^19.0.0
+ '@types/react-dom': ^18.0.0 || ^19.0.0
+ react: ^18.0.0 || ^19.0.0
+ react-dom: ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
'@tootallnate/quickjs-emscripten@0.23.0':
resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==}
@@ -2541,6 +2606,9 @@ packages:
peerDependencies:
ajv: 4.11.8 - 8
+ bidi-js@1.0.3:
+ resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==}
+
binary-extensions@2.3.0:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'}
@@ -2872,6 +2940,14 @@ packages:
css-to-react-native@3.2.0:
resolution: {integrity: sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==}
+ css-tree@3.1.0:
+ resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==}
+ engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
+
+ cssstyle@5.3.5:
+ resolution: {integrity: sha512-GlsEptulso7Jg0VaOZ8BXQi3AkYM5BOJKEO/rjMidSCq70FkIC5y0eawrCXeYzxgt3OCf4Ls+eoxN+/05vN0Ag==}
+ engines: {node: '>=20'}
+
csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
@@ -2886,6 +2962,10 @@ packages:
resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==}
engines: {node: '>= 14'}
+ data-urls@6.0.0:
+ resolution: {integrity: sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==}
+ engines: {node: '>=20'}
+
data-view-buffer@1.0.2:
resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==}
engines: {node: '>= 0.4'}
@@ -2907,6 +2987,9 @@ packages:
supports-color:
optional: true
+ decimal.js@10.6.0:
+ resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
+
decko@1.2.0:
resolution: {integrity: sha512-m8FnyHXV1QX+S1cl+KPFDIl6NMkxtKsy6+U/aYyjrOqWMuwAwYWu7ePqrsUHtDR5Y8Yk2pi/KIDSgF+vT4cPOQ==}
@@ -3062,6 +3145,10 @@ packages:
end-of-stream@1.4.5:
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
+ entities@6.0.1:
+ resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
+ engines: {node: '>=0.12'}
+
error-ex@1.3.4:
resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==}
@@ -3530,6 +3617,10 @@ packages:
hmac-drbg@1.0.1:
resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==}
+ html-encoding-sniffer@4.0.0:
+ resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==}
+ engines: {node: '>=18'}
+
html-escaper@2.0.2:
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
@@ -3564,6 +3655,10 @@ packages:
engines: {node: '>=18'}
hasBin: true
+ iconv-lite@0.6.3:
+ resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
+ engines: {node: '>=0.10.0'}
+
iconv-lite@0.7.0:
resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==}
engines: {node: '>=0.10.0'}
@@ -3712,6 +3807,9 @@ packages:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
+ is-potential-custom-element-name@1.0.1:
+ resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
+
is-promise@4.0.0:
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
@@ -3832,6 +3930,15 @@ packages:
jsbn@1.1.0:
resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==}
+ jsdom@27.3.0:
+ resolution: {integrity: sha512-GtldT42B8+jefDUC4yUKAvsaOrH7PDHmZxZXNgF2xMmymjUbRYJvpAybZAKEmXDGTM0mCsz8duOa4vTm5AY2Kg==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
+ peerDependencies:
+ canvas: ^3.0.0
+ peerDependenciesMeta:
+ canvas:
+ optional: true
+
jsesc@3.1.0:
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
engines: {node: '>=6'}
@@ -3986,6 +4093,9 @@ packages:
md5.js@1.3.5:
resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==}
+ mdn-data@2.12.2:
+ resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==}
+
media-typer@1.1.0:
resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==}
engines: {node: '>= 0.8'}
@@ -4392,6 +4502,9 @@ packages:
resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==}
engines: {node: '>=18'}
+ parse5@8.0.0:
+ resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==}
+
parseurl@1.3.3:
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
engines: {node: '>= 0.8'}
@@ -4814,6 +4927,10 @@ packages:
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
+ saxes@6.0.0:
+ resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
+ engines: {node: '>=v12.22.7'}
+
scheduler@0.23.2:
resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
@@ -5114,6 +5231,9 @@ packages:
resolution: {integrity: sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g==}
hasBin: true
+ symbol-tree@3.2.4:
+ resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
+
synckit@0.11.11:
resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==}
engines: {node: ^14.18.0 || >=16.0.0}
@@ -5187,6 +5307,13 @@ packages:
resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==}
engines: {node: '>=14.0.0'}
+ tldts-core@7.0.19:
+ resolution: {integrity: sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==}
+
+ tldts@7.0.19:
+ resolution: {integrity: sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==}
+ hasBin: true
+
tmp@0.2.5:
resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==}
engines: {node: '>=14.14'}
@@ -5203,6 +5330,10 @@ packages:
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
engines: {node: '>=0.6'}
+ tough-cookie@6.0.0:
+ resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==}
+ engines: {node: '>=16'}
+
tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
@@ -5210,6 +5341,10 @@ packages:
resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==}
engines: {node: '>=18'}
+ tr46@6.0.0:
+ resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==}
+ engines: {node: '>=20'}
+
tree-kill@1.2.2:
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
hasBin: true
@@ -5523,6 +5658,10 @@ packages:
zod:
optional: true
+ w3c-xmlserializer@5.0.0:
+ resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
+ engines: {node: '>=18'}
+
walk-up-path@4.0.0:
resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==}
engines: {node: 20 || >=22}
@@ -5538,10 +5677,26 @@ packages:
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
engines: {node: '>=12'}
+ webidl-conversions@8.0.0:
+ resolution: {integrity: sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==}
+ engines: {node: '>=20'}
+
+ whatwg-encoding@3.1.1:
+ resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
+ engines: {node: '>=18'}
+
+ whatwg-mimetype@4.0.0:
+ resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
+ engines: {node: '>=18'}
+
whatwg-url@14.2.0:
resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==}
engines: {node: '>=18'}
+ whatwg-url@15.1.0:
+ resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==}
+ engines: {node: '>=20'}
+
whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
@@ -5621,6 +5776,13 @@ packages:
resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==}
engines: {node: '>=18'}
+ xml-name-validator@5.0.0:
+ resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
+ engines: {node: '>=18'}
+
+ xmlchars@2.2.0:
+ resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
+
xtend@2.1.2:
resolution: {integrity: sha512-vMNKzr2rHP9Dp/e1NQFnLQlwlhp9L/LfvnsVdHxN1f+uggyVI3i08uD14GPvCToPkdsRfyPqIyYGmIk58V98ZQ==}
engines: {node: '>=0.4'}
@@ -5695,6 +5857,8 @@ packages:
snapshots:
+ '@acemir/cssom@0.9.29': {}
+
'@ai-sdk/azure@2.0.71(zod@3.25.76)':
dependencies:
'@ai-sdk/openai': 2.0.69(zod@3.25.76)
@@ -5751,6 +5915,24 @@ snapshots:
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
+ '@asamuzakjp/css-color@4.1.1':
+ dependencies:
+ '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
+ '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
+ '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
+ '@csstools/css-tokenizer': 3.0.4
+ lru-cache: 11.2.4
+
+ '@asamuzakjp/dom-selector@6.7.6':
+ dependencies:
+ '@asamuzakjp/nwsapi': 2.3.9
+ bidi-js: 1.0.3
+ css-tree: 3.1.0
+ is-potential-custom-element-name: 1.0.1
+ lru-cache: 11.2.4
+
+ '@asamuzakjp/nwsapi@2.3.9': {}
+
'@babel/code-frame@7.27.1':
dependencies:
'@babel/helper-validator-identifier': 7.28.5
@@ -5873,6 +6055,28 @@ snapshots:
dependencies:
'@jridgewell/trace-mapping': 0.3.9
+ '@csstools/color-helpers@5.1.0': {}
+
+ '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
+ dependencies:
+ '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
+ '@csstools/css-tokenizer': 3.0.4
+
+ '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
+ dependencies:
+ '@csstools/color-helpers': 5.1.0
+ '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
+ '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
+ '@csstools/css-tokenizer': 3.0.4
+
+ '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)':
+ dependencies:
+ '@csstools/css-tokenizer': 3.0.4
+
+ '@csstools/css-syntax-patches-for-csstree@1.0.22': {}
+
+ '@csstools/css-tokenizer@3.0.4': {}
+
'@emnapi/core@1.7.1':
dependencies:
'@emnapi/wasi-threads': 1.1.0
@@ -7585,6 +7789,16 @@ snapshots:
lz-string: 1.5.0
pretty-format: 27.5.1
+ '@testing-library/react@16.3.1(@testing-library/dom@9.3.1)(@types/react-dom@19.2.3(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@babel/runtime': 7.28.4
+ '@testing-library/dom': 9.3.1
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.27
+ '@types/react-dom': 19.2.3(@types/react@18.3.27)
+
'@tootallnate/quickjs-emscripten@0.23.0': {}
'@tsconfig/node10@1.0.12': {}
@@ -7896,7 +8110,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@24.10.1)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))':
+ '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.3.0)(tsx@4.21.0)(yaml@2.8.2))':
dependencies:
'@ampproject/remapping': 2.3.0
'@bcoe/v8-coverage': 1.0.2
@@ -7911,18 +8125,18 @@ snapshots:
std-env: 3.10.0
test-exclude: 7.0.1
tinyrainbow: 2.0.0
- vitest: 3.2.4(@types/node@24.10.1)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)
+ vitest: 3.2.4(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.3.0)(tsx@4.21.0)(yaml@2.8.2)
transitivePeerDependencies:
- supports-color
- '@vitest/eslint-plugin@1.4.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)(vitest@3.2.4(@types/node@24.10.1)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))':
+ '@vitest/eslint-plugin@1.4.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)(vitest@3.2.4(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.3.0)(tsx@4.21.0)(yaml@2.8.2))':
dependencies:
'@typescript-eslint/scope-manager': 8.47.0
'@typescript-eslint/utils': 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
eslint: 9.39.1(jiti@2.6.1)
optionalDependencies:
typescript: 5.9.3
- vitest: 3.2.4(@types/node@24.10.1)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)
+ vitest: 3.2.4(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.3.0)(tsx@4.21.0)(yaml@2.8.2)
transitivePeerDependencies:
- supports-color
@@ -8194,6 +8408,10 @@ snapshots:
jsonpointer: 5.0.1
leven: 3.1.0
+ bidi-js@1.0.3:
+ dependencies:
+ require-from-string: 2.0.2
+
binary-extensions@2.3.0: {}
bindings@1.5.0:
@@ -8589,6 +8807,17 @@ snapshots:
css-color-keywords: 1.0.0
postcss-value-parser: 4.2.0
+ css-tree@3.1.0:
+ dependencies:
+ mdn-data: 2.12.2
+ source-map-js: 1.2.1
+
+ cssstyle@5.3.5:
+ dependencies:
+ '@asamuzakjp/css-color': 4.1.1
+ '@csstools/css-syntax-patches-for-csstree': 1.0.22
+ css-tree: 3.1.0
+
csstype@3.1.3: {}
csstype@3.2.3: {}
@@ -8597,6 +8826,11 @@ snapshots:
data-uri-to-buffer@6.0.2: {}
+ data-urls@6.0.0:
+ dependencies:
+ whatwg-mimetype: 4.0.0
+ whatwg-url: 15.1.0
+
data-view-buffer@1.0.2:
dependencies:
call-bound: 1.0.4
@@ -8621,6 +8855,8 @@ snapshots:
optionalDependencies:
supports-color: 10.2.2
+ decimal.js@10.6.0: {}
+
decko@1.2.0: {}
decompress-response@6.0.0:
@@ -8823,6 +9059,8 @@ snapshots:
dependencies:
once: 1.4.0
+ entities@6.0.1: {}
+
error-ex@1.3.4:
dependencies:
is-arrayish: 0.2.1
@@ -9468,6 +9706,10 @@ snapshots:
minimalistic-assert: 1.0.1
minimalistic-crypto-utils: 1.0.1
+ html-encoding-sniffer@4.0.0:
+ dependencies:
+ whatwg-encoding: 3.1.1
+
html-escaper@2.0.2: {}
html-tokenize@2.0.1:
@@ -9514,6 +9756,10 @@ snapshots:
husky@9.1.7: {}
+ iconv-lite@0.6.3:
+ dependencies:
+ safer-buffer: 2.1.2
+
iconv-lite@0.7.0:
dependencies:
safer-buffer: 2.1.2
@@ -9651,6 +9897,8 @@ snapshots:
is-number@7.0.0: {}
+ is-potential-custom-element-name@1.0.1: {}
+
is-promise@4.0.0: {}
is-regex@1.2.1:
@@ -9762,6 +10010,33 @@ snapshots:
jsbn@1.1.0: {}
+ jsdom@27.3.0:
+ dependencies:
+ '@acemir/cssom': 0.9.29
+ '@asamuzakjp/dom-selector': 6.7.6
+ cssstyle: 5.3.5
+ data-urls: 6.0.0
+ decimal.js: 10.6.0
+ html-encoding-sniffer: 4.0.0
+ http-proxy-agent: 7.0.2
+ https-proxy-agent: 7.0.6(supports-color@10.2.2)
+ is-potential-custom-element-name: 1.0.1
+ parse5: 8.0.0
+ saxes: 6.0.0
+ symbol-tree: 3.2.4
+ tough-cookie: 6.0.0
+ w3c-xmlserializer: 5.0.0
+ webidl-conversions: 8.0.0
+ whatwg-encoding: 3.1.1
+ whatwg-mimetype: 4.0.0
+ whatwg-url: 15.1.0
+ ws: 8.18.3
+ xml-name-validator: 5.0.0
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+
jsesc@3.1.0: {}
json-buffer@3.0.1: {}
@@ -9904,6 +10179,8 @@ snapshots:
inherits: 2.0.4
safe-buffer: 5.2.1
+ mdn-data@2.12.2: {}
+
media-typer@1.1.0: {}
memory-pager@1.5.0: {}
@@ -10394,6 +10671,10 @@ snapshots:
index-to-position: 1.2.0
type-fest: 4.41.0
+ parse5@8.0.0:
+ dependencies:
+ entities: 6.0.1
+
parseurl@1.3.3: {}
path-browserify@1.0.1: {}
@@ -10896,6 +11177,10 @@ snapshots:
safer-buffer@2.1.2: {}
+ saxes@6.0.0:
+ dependencies:
+ xmlchars: 2.2.0
+
scheduler@0.23.2:
dependencies:
loose-envify: 1.4.0
@@ -11287,6 +11572,8 @@ snapshots:
transitivePeerDependencies:
- encoding
+ symbol-tree@3.2.4: {}
+
synckit@0.11.11:
dependencies:
'@pkgr/core': 0.2.9
@@ -11414,6 +11701,12 @@ snapshots:
tinyspy@4.0.4: {}
+ tldts-core@7.0.19: {}
+
+ tldts@7.0.19:
+ dependencies:
+ tldts-core: 7.0.19
+
tmp@0.2.5: {}
to-buffer@1.2.2:
@@ -11428,12 +11721,20 @@ snapshots:
toidentifier@1.0.1: {}
+ tough-cookie@6.0.0:
+ dependencies:
+ tldts: 7.0.19
+
tr46@0.0.3: {}
tr46@5.1.1:
dependencies:
punycode: 2.3.1
+ tr46@6.0.0:
+ dependencies:
+ punycode: 2.3.1
+
tree-kill@1.2.2: {}
ts-algebra@1.2.2: {}
@@ -11680,7 +11981,7 @@ snapshots:
tsx: 4.21.0
yaml: 2.8.2
- vitest@3.2.4(@types/node@24.10.1)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2):
+ vitest@3.2.4(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.3.0)(tsx@4.21.0)(yaml@2.8.2):
dependencies:
'@types/chai': 5.2.3
'@vitest/expect': 3.2.4
@@ -11707,6 +12008,7 @@ snapshots:
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 24.10.1
+ jsdom: 27.3.0
transitivePeerDependencies:
- jiti
- less
@@ -11730,6 +12032,10 @@ snapshots:
optionalDependencies:
zod: 3.25.76
+ w3c-xmlserializer@5.0.0:
+ dependencies:
+ xml-name-validator: 5.0.0
+
walk-up-path@4.0.0: {}
web-streams-polyfill@3.3.3: {}
@@ -11738,11 +12044,24 @@ snapshots:
webidl-conversions@7.0.0: {}
+ webidl-conversions@8.0.0: {}
+
+ whatwg-encoding@3.1.1:
+ dependencies:
+ iconv-lite: 0.6.3
+
+ whatwg-mimetype@4.0.0: {}
+
whatwg-url@14.2.0:
dependencies:
tr46: 5.1.1
webidl-conversions: 7.0.0
+ whatwg-url@15.1.0:
+ dependencies:
+ tr46: 6.0.0
+ webidl-conversions: 8.0.0
+
whatwg-url@5.0.0:
dependencies:
tr46: 0.0.3
@@ -11830,6 +12149,10 @@ snapshots:
dependencies:
is-wsl: 3.1.0
+ xml-name-validator@5.0.0: {}
+
+ xmlchars@2.2.0: {}
+
xtend@2.1.2:
dependencies:
object-keys: 0.4.0
diff --git a/src/ui/hooks/index.ts b/src/ui/hooks/index.ts
index e9d3be4c8..168af5dd2 100644
--- a/src/ui/hooks/index.ts
+++ b/src/ui/hooks/index.ts
@@ -1 +1,2 @@
export { useRenderData } from "./useRenderData.js";
+export { useHostCommunication } from "./useHostCommunication.js";
diff --git a/src/ui/hooks/useHostCommunication.ts b/src/ui/hooks/useHostCommunication.ts
new file mode 100644
index 000000000..ae07a97b6
--- /dev/null
+++ b/src/ui/hooks/useHostCommunication.ts
@@ -0,0 +1,76 @@
+import { useCallback } from "react";
+import {
+ postUIActionResult,
+ uiActionResultIntent,
+ uiActionResultNotification,
+ uiActionResultPrompt,
+ uiActionResultToolCall,
+ uiActionResultLink,
+} from "@mcp-ui/server";
+
+/** Return type for the useHostCommunication hook */
+interface UseHostCommunicationResult {
+ /** Sends an intent message for the host to act on */
+ intent: typeof uiActionResultIntent;
+ /** Notifies the host of something that happened */
+ notify: typeof uiActionResultNotification;
+ /** Asks the host to run a prompt */
+ prompt: typeof uiActionResultPrompt;
+ /** Asks the host to execute a tool */
+ tool: typeof uiActionResultToolCall;
+ /** Asks the host to navigate to a URL */
+ link: typeof uiActionResultLink;
+}
+
+/**
+ * Hook for sending UI actions to the parent window via postMessage
+ * This is used by iframe-based UI components to communicate back to an MCP client
+ *
+ * @example
+ * ```tsx
+ * function MyComponent() {
+ * const { intent, tool, link } = useHostCommunication();
+ *
+ * return ;
+ * }
+ * ```
+ */
+export function useHostCommunication(): UseHostCommunicationResult {
+ const intent: typeof uiActionResultIntent = useCallback((...args) => {
+ const result = uiActionResultIntent(...args);
+ postUIActionResult(result);
+ return result;
+ }, []);
+
+ const notify: typeof uiActionResultNotification = useCallback((...args) => {
+ const result = uiActionResultNotification(...args);
+ postUIActionResult(result);
+ return result;
+ }, []);
+
+ const prompt: typeof uiActionResultPrompt = useCallback((...args) => {
+ const result = uiActionResultPrompt(...args);
+ postUIActionResult(result);
+ return result;
+ }, []);
+
+ const tool: typeof uiActionResultToolCall = useCallback((...args) => {
+ const result = uiActionResultToolCall(...args);
+ postUIActionResult(result);
+ return result;
+ }, []);
+
+ const link: typeof uiActionResultLink = useCallback((...args) => {
+ const result = uiActionResultLink(...args);
+ postUIActionResult(result);
+ return result;
+ }, []);
+
+ return {
+ intent,
+ notify,
+ prompt,
+ tool,
+ link,
+ };
+}
diff --git a/src/ui/hooks/useRenderData.ts b/src/ui/hooks/useRenderData.ts
index 461bbc983..6556b7f63 100644
--- a/src/ui/hooks/useRenderData.ts
+++ b/src/ui/hooks/useRenderData.ts
@@ -33,7 +33,6 @@ interface UseRenderDataResult {
*
* function MyComponent() {
* const { data, isLoading, error } = useRenderData();
- * // ...
* }
* ```
*/
diff --git a/tests/unit/ui/useHostCommunication.test.ts b/tests/unit/ui/useHostCommunication.test.ts
new file mode 100644
index 000000000..af9a9bd9f
--- /dev/null
+++ b/tests/unit/ui/useHostCommunication.test.ts
@@ -0,0 +1,155 @@
+/**
+ * @vitest-environment jsdom
+ */
+import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
+import { renderHook } from "@testing-library/react";
+import { useHostCommunication } from "../../../src/ui/hooks/useHostCommunication.js";
+
+describe("useHostCommunication", () => {
+ let postMessageMock: ReturnType;
+ let originalParent: typeof window.parent;
+
+ beforeEach(() => {
+ postMessageMock = vi.fn();
+ originalParent = window.parent;
+
+ // Mock window.parent.postMessage without replacing the entire window object
+ Object.defineProperty(window, "parent", {
+ value: { postMessage: postMessageMock },
+ writable: true,
+ configurable: true,
+ });
+ });
+
+ afterEach(() => {
+ Object.defineProperty(window, "parent", {
+ value: originalParent,
+ writable: true,
+ configurable: true,
+ });
+ vi.restoreAllMocks();
+ });
+
+ it("intent() sends a message with name and params", () => {
+ const { result } = renderHook(() => useHostCommunication());
+
+ result.current.intent("create-task", { title: "Test Task" });
+
+ expect(postMessageMock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ type: "intent",
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ payload: expect.objectContaining({
+ intent: "create-task",
+ params: { title: "Test Task" },
+ }),
+ }),
+ "*"
+ );
+ });
+
+ it("intent() sends a message with empty params", () => {
+ const { result } = renderHook(() => useHostCommunication());
+
+ result.current.intent("cancel", {});
+
+ expect(postMessageMock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ type: "intent",
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ payload: expect.objectContaining({
+ intent: "cancel",
+ params: {},
+ }),
+ }),
+ "*"
+ );
+ });
+
+ it("notify() sends a notification message", () => {
+ const { result } = renderHook(() => useHostCommunication());
+
+ result.current.notify("Operation completed successfully");
+
+ expect(postMessageMock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ type: "notify",
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ payload: expect.objectContaining({
+ message: "Operation completed successfully",
+ }),
+ }),
+ "*"
+ );
+ });
+
+ it("prompt() sends a prompt message", () => {
+ const { result } = renderHook(() => useHostCommunication());
+
+ result.current.prompt("What is the status of my database?");
+
+ expect(postMessageMock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ type: "prompt",
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ payload: expect.objectContaining({
+ prompt: "What is the status of my database?",
+ }),
+ }),
+ "*"
+ );
+ });
+
+ it("tool() sends a tool message with name and params", () => {
+ const { result } = renderHook(() => useHostCommunication());
+
+ result.current.tool("listDatabases", { connectionString: "mongodb://localhost" });
+
+ expect(postMessageMock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ type: "tool",
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ payload: expect.objectContaining({
+ toolName: "listDatabases",
+ params: { connectionString: "mongodb://localhost" },
+ }),
+ }),
+ "*"
+ );
+ });
+
+ it("tool() sends a tool message with empty params", () => {
+ const { result } = renderHook(() => useHostCommunication());
+
+ result.current.tool("getServerInfo", {});
+
+ expect(postMessageMock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ type: "tool",
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ payload: expect.objectContaining({
+ toolName: "getServerInfo",
+ params: {},
+ }),
+ }),
+ "*"
+ );
+ });
+
+ it("link() sends a link message with a URL", () => {
+ const { result } = renderHook(() => useHostCommunication());
+
+ result.current.link("https://mongodb.com/docs");
+
+ expect(postMessageMock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ type: "link",
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ payload: expect.objectContaining({
+ url: "https://mongodb.com/docs",
+ }),
+ }),
+ "*"
+ );
+ });
+});
diff --git a/tests/unit/ui/useRenderData.test.ts b/tests/unit/ui/useRenderData.test.ts
new file mode 100644
index 000000000..4a38d6ecb
--- /dev/null
+++ b/tests/unit/ui/useRenderData.test.ts
@@ -0,0 +1,62 @@
+/**
+ * @vitest-environment jsdom
+ */
+import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
+import { renderHook } from "@testing-library/react";
+import { useRenderData } from "../../../src/ui/hooks/useRenderData.js";
+
+interface TestData {
+ items: string[];
+}
+
+describe("useRenderData", () => {
+ let postMessageMock: ReturnType;
+ let originalParent: typeof window.parent;
+
+ beforeEach(() => {
+ postMessageMock = vi.fn();
+ originalParent = window.parent;
+
+ // Mock window.parent.postMessage without replacing the entire window object
+ Object.defineProperty(window, "parent", {
+ value: { postMessage: postMessageMock },
+ writable: true,
+ configurable: true,
+ });
+ });
+
+ afterEach(() => {
+ Object.defineProperty(window, "parent", {
+ value: originalParent,
+ writable: true,
+ configurable: true,
+ });
+ vi.restoreAllMocks();
+ });
+
+ it("returns initial state with isLoading true", () => {
+ const { result } = renderHook(() => useRenderData());
+
+ expect(result.current.data).toBeNull();
+ expect(result.current.isLoading).toBe(true);
+ expect(result.current.error).toBeNull();
+ });
+
+ it("includes expected properties in return type", () => {
+ const { result } = renderHook(() => useRenderData());
+
+ expect(result.current).toHaveProperty("data");
+ expect(result.current).toHaveProperty("isLoading");
+ expect(result.current).toHaveProperty("error");
+ });
+
+ it("returns a stable object shape for destructuring", () => {
+ const { result } = renderHook(() => useRenderData());
+
+ const { data, isLoading, error } = result.current;
+
+ expect(data).toBeNull();
+ expect(isLoading).toBe(true);
+ expect(error).toBeNull();
+ });
+});