feat(apps): Integrate AppRenderer from @mcp-ui/client SDK (#7013)
This commit is contained in:
@@ -138,7 +138,7 @@
|
||||
.parent
|
||||
.postMessage({
|
||||
jsonrpc: '2.0',
|
||||
method: 'ui/notifications/sandbox-ready',
|
||||
method: 'ui/notifications/sandbox-proxy-ready',
|
||||
params: {}
|
||||
}, '*');
|
||||
})();
|
||||
|
||||
@@ -12,6 +12,12 @@ pub struct CspMetadata {
|
||||
/// Domains allowed for resource loading (scripts, styles, images, fonts, media)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub resource_domains: Option<Vec<String>>,
|
||||
/// Domains allowed for frame-src (nested iframes)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub frame_domains: Option<Vec<String>>,
|
||||
/// Domains allowed for base-uri
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub base_uri_domains: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// Sandbox permissions for MCP Apps
|
||||
|
||||
@@ -3751,6 +3751,14 @@
|
||||
"type": "object",
|
||||
"description": "Content Security Policy metadata for MCP Apps\nSpecifies allowed domains for network connections and resource loading",
|
||||
"properties": {
|
||||
"baseUriDomains": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Domains allowed for base-uri",
|
||||
"nullable": true
|
||||
},
|
||||
"connectDomains": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -3759,6 +3767,14 @@
|
||||
"description": "Domains allowed for connect-src (fetch, XHR, WebSocket)",
|
||||
"nullable": true
|
||||
},
|
||||
"frameDomains": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Domains allowed for frame-src (nested iframes)",
|
||||
"nullable": true
|
||||
},
|
||||
"resourceDomains": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
||||
Generated
+395
-6
@@ -9,7 +9,8 @@
|
||||
"version": "1.23.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@mcp-ui/client": "^5.17.3",
|
||||
"@mcp-ui/client": "^6.0.0",
|
||||
"@modelcontextprotocol/ext-apps": "^1.0.1",
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
"@radix-ui/react-avatar": "^1.1.11",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
@@ -3027,27 +3028,272 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mcp-ui/client": {
|
||||
"version": "5.17.3",
|
||||
"resolved": "https://registry.npmjs.org/@mcp-ui/client/-/client-5.17.3.tgz",
|
||||
"integrity": "sha512-Xxi8d5NYbCBUdBEqhwnm7PgJE6+F1wOV2sPA0AWnfLoudpiq8iomovL/AMFI+alVZwhSBxzqJKqUdbPD2yz5tQ==",
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@mcp-ui/client/-/client-6.0.0.tgz",
|
||||
"integrity": "sha512-dHIQGjFOoBWBntSRUJH5YFeq7xi2rEPS0EwokeNAnMg6xrjGjvNd6vTWDHFRC04OlO/ogvM1r5+xUoo0OETaaQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.22.0",
|
||||
"@modelcontextprotocol/ext-apps": "^0.3.1",
|
||||
"@modelcontextprotocol/sdk": "^1.24.0",
|
||||
"@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"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18 || ^19",
|
||||
"react-dom": "^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/@mcp-ui/client/node_modules/@modelcontextprotocol/ext-apps": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/ext-apps/-/ext-apps-0.3.1.tgz",
|
||||
"integrity": "sha512-Iivz2KwWK8xlRbiWwFB/C4NXqE8VJBoRCbBkJCN98ST2UbQvA6kfyebcLsypiqylJS467XOOaBcI9DeQ3t+zqA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"examples/*"
|
||||
],
|
||||
"optionalDependencies": {
|
||||
"@oven/bun-darwin-aarch64": "^1.2.21",
|
||||
"@oven/bun-darwin-x64": "^1.2.21",
|
||||
"@oven/bun-darwin-x64-baseline": "^1.2.21",
|
||||
"@oven/bun-linux-aarch64": "^1.2.21",
|
||||
"@oven/bun-linux-aarch64-musl": "^1.2.21",
|
||||
"@oven/bun-linux-x64": "^1.2.21",
|
||||
"@oven/bun-linux-x64-baseline": "^1.2.21",
|
||||
"@oven/bun-linux-x64-musl": "^1.2.21",
|
||||
"@oven/bun-linux-x64-musl-baseline": "^1.2.21",
|
||||
"@oven/bun-windows-x64": "^1.2.21",
|
||||
"@oven/bun-windows-x64-baseline": "^1.2.21",
|
||||
"@rollup/rollup-darwin-arm64": "^4.53.3",
|
||||
"@rollup/rollup-darwin-x64": "^4.53.3",
|
||||
"@rollup/rollup-linux-arm64-gnu": "^4.53.3",
|
||||
"@rollup/rollup-linux-x64-gnu": "^4.53.3",
|
||||
"@rollup/rollup-win32-arm64-msvc": "^4.53.3",
|
||||
"@rollup/rollup-win32-x64-msvc": "^4.53.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.24.0",
|
||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mcp-ui/client/node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz",
|
||||
"integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@mcp-ui/client/node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz",
|
||||
"integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@mcp-ui/client/node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz",
|
||||
"integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@mcp-ui/client/node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz",
|
||||
"integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@mcp-ui/client/node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz",
|
||||
"integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@mcp-ui/client/node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz",
|
||||
"integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/ext-apps": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/ext-apps/-/ext-apps-1.0.1.tgz",
|
||||
"integrity": "sha512-rAPzBbB5GNgYk216paQjGKUgbNXSy/yeR95c0ni6Y4uvhWI2AeF+ztEOqQFLBMQy/MPM+02pbVK1HaQmQjMwYQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"examples/*"
|
||||
],
|
||||
"optionalDependencies": {
|
||||
"@oven/bun-darwin-aarch64": "^1.2.21",
|
||||
"@oven/bun-darwin-x64": "^1.2.21",
|
||||
"@oven/bun-darwin-x64-baseline": "^1.2.21",
|
||||
"@oven/bun-linux-aarch64": "^1.2.21",
|
||||
"@oven/bun-linux-aarch64-musl": "^1.2.21",
|
||||
"@oven/bun-linux-x64": "^1.2.21",
|
||||
"@oven/bun-linux-x64-baseline": "^1.2.21",
|
||||
"@oven/bun-linux-x64-musl": "^1.2.21",
|
||||
"@oven/bun-linux-x64-musl-baseline": "^1.2.21",
|
||||
"@oven/bun-windows-x64": "^1.2.21",
|
||||
"@oven/bun-windows-x64-baseline": "^1.2.21",
|
||||
"@rollup/rollup-darwin-arm64": "^4.53.3",
|
||||
"@rollup/rollup-darwin-x64": "^4.53.3",
|
||||
"@rollup/rollup-linux-arm64-gnu": "^4.53.3",
|
||||
"@rollup/rollup-linux-x64-gnu": "^4.53.3",
|
||||
"@rollup/rollup-win32-arm64-msvc": "^4.53.3",
|
||||
"@rollup/rollup-win32-x64-msvc": "^4.53.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.24.0",
|
||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/ext-apps/node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz",
|
||||
"integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/ext-apps/node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz",
|
||||
"integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/ext-apps/node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz",
|
||||
"integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/ext-apps/node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz",
|
||||
"integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/ext-apps/node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz",
|
||||
"integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/ext-apps/node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz",
|
||||
"integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk": {
|
||||
"version": "1.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.2.tgz",
|
||||
"integrity": "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.19.7",
|
||||
"ajv": "^8.17.1",
|
||||
@@ -3214,6 +3460,149 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@oven/bun-darwin-aarch64": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@oven/bun-darwin-aarch64/-/bun-darwin-aarch64-1.3.8.tgz",
|
||||
"integrity": "sha512-hPERz4IgXCM6Y6GdEEsJAFceyJMt29f3HlFzsvE/k+TQjChRhar6S+JggL35b9VmFfsdxyCOOTPqgnSrdV0etA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@oven/bun-darwin-x64": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@oven/bun-darwin-x64/-/bun-darwin-x64-1.3.8.tgz",
|
||||
"integrity": "sha512-SaWIxsRQYiT/eA60bqA4l8iNO7cJ6YD8ie82RerRp9voceBxPIZiwX4y20cTKy5qNaSGr9LxfYq7vDywTipiog==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@oven/bun-darwin-x64-baseline": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@oven/bun-darwin-x64-baseline/-/bun-darwin-x64-baseline-1.3.8.tgz",
|
||||
"integrity": "sha512-ArHVWpCRZI3vGLoN2/8ud8Kzqlgn1Gv+fNw+pMB9x18IzgAEhKxFxsWffnoaH21amam4tAOhpeewRIgdNtB0Cw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@oven/bun-linux-aarch64": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@oven/bun-linux-aarch64/-/bun-linux-aarch64-1.3.8.tgz",
|
||||
"integrity": "sha512-rq0nNckobtS+ONoB95/Frfqr8jCtmSjjjEZlN4oyUx0KEBV11Vj4v3cDVaWzuI34ryL8FCog3HaqjfKn8R82Tw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@oven/bun-linux-aarch64-musl": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@oven/bun-linux-aarch64-musl/-/bun-linux-aarch64-musl-1.3.8.tgz",
|
||||
"integrity": "sha512-HvJmhrfipL7GtuqFz6xNpmf27NGcCOMwCalPjNR6fvkLpe8A7Z1+QbxKKjOglelmlmZc3Vi2TgDUtxSqfqOToQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@oven/bun-linux-x64": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@oven/bun-linux-x64/-/bun-linux-x64-1.3.8.tgz",
|
||||
"integrity": "sha512-YDgqVx1MI8E0oDbCEUSkAMBKKGnUKfaRtMdLh9Bjhu7JQacQ/ZCpxwi4HPf5Q0O1TbWRrdxGw2tA2Ytxkn7s1Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@oven/bun-linux-x64-baseline": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@oven/bun-linux-x64-baseline/-/bun-linux-x64-baseline-1.3.8.tgz",
|
||||
"integrity": "sha512-3IkS3TuVOzMqPW6Gg9/8FEoKF/rpKZ9DZUfNy9GQ54+k4PGcXpptU3+dy8D4iDFCt4qe6bvoiAOdM44OOsZ+Wg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@oven/bun-linux-x64-musl": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@oven/bun-linux-x64-musl/-/bun-linux-x64-musl-1.3.8.tgz",
|
||||
"integrity": "sha512-o7Jm5zL4aw9UBs3BcZLVbgGm2V4F10MzAQAV+ziKzoEfYmYtvDqRVxgKEq7BzUOVy4LgfrfwzEXw5gAQGRrhQQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@oven/bun-linux-x64-musl-baseline": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@oven/bun-linux-x64-musl-baseline/-/bun-linux-x64-musl-baseline-1.3.8.tgz",
|
||||
"integrity": "sha512-5g8XJwHhcTh8SGoKO7pR54ILYDbuFkGo+68DOMTiVB5eLxuLET+Or/camHgk4QWp3nUS5kNjip4G8BE8i0rHVQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@oven/bun-windows-x64": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@oven/bun-windows-x64/-/bun-windows-x64-1.3.8.tgz",
|
||||
"integrity": "sha512-UDI3rowMm/tI6DIynpE4XqrOhr+1Ztk1NG707Wxv2nygup+anTswgCwjfjgmIe78LdoRNFrux2GpeolhQGW6vQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@oven/bun-windows-x64-baseline": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@oven/bun-windows-x64-baseline/-/bun-windows-x64-baseline-1.3.8.tgz",
|
||||
"integrity": "sha512-K6qBUKAZLXsjAwFxGTG87dsWlDjyDl2fqjJr7+x7lmv2m+aSEzmLOK+Z5pSvGkpjBp3LXV35UUgj8G0UTd0pPg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-android-arm-eabi": {
|
||||
"version": "11.16.2",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.16.2.tgz",
|
||||
|
||||
@@ -39,7 +39,8 @@
|
||||
"start-alpha-gui": "ALPHA=true npm run start-gui"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mcp-ui/client": "^5.17.3",
|
||||
"@mcp-ui/client": "^6.0.0",
|
||||
"@modelcontextprotocol/ext-apps": "^1.0.1",
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
"@radix-ui/react-avatar": "^1.1.11",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
|
||||
@@ -143,10 +143,18 @@ export type CreateScheduleRequest = {
|
||||
* Specifies allowed domains for network connections and resource loading
|
||||
*/
|
||||
export type CspMetadata = {
|
||||
/**
|
||||
* Domains allowed for base-uri
|
||||
*/
|
||||
baseUriDomains?: Array<string> | null;
|
||||
/**
|
||||
* Domains allowed for connect-src (fetch, XHR, WebSocket)
|
||||
*/
|
||||
connectDomains?: Array<string> | null;
|
||||
/**
|
||||
* Domains allowed for frame-src (nested iframes)
|
||||
*/
|
||||
frameDomains?: Array<string> | null;
|
||||
/**
|
||||
* Domains allowed for resource loading (scripts, styles, images, fonts, media)
|
||||
*/
|
||||
|
||||
@@ -1,50 +1,178 @@
|
||||
import { AppEvents } from '../../constants/events';
|
||||
/**
|
||||
* MCP Apps Renderer
|
||||
* McpAppRenderer — Renders interactive MCP App UIs inside a sandboxed iframe.
|
||||
*
|
||||
* Temporary Goose implementation while waiting for official SDK components.
|
||||
* This component implements the host side of the MCP Apps protocol using the
|
||||
* @mcp-ui/client SDK's AppRenderer. It handles resource fetching, sandbox
|
||||
* proxy setup, CSP enforcement, and bidirectional communication with guest apps.
|
||||
*
|
||||
* @see SEP-1865 https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx
|
||||
* Protocol references:
|
||||
* - MCP Apps Extension (ext-apps): https://github.com/modelcontextprotocol/ext-apps
|
||||
* - MCP-UI Client SDK: https://github.com/idosal/mcp-ui
|
||||
* - App Bridge types: @modelcontextprotocol/ext-apps/app-bridge
|
||||
*
|
||||
* Display modes:
|
||||
* - "inline" | "fullscreen" | "pip" — standard MCP display modes
|
||||
* - "standalone" — Goose-specific mode for dedicated Electron windows
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { useSandboxBridge } from './useSandboxBridge';
|
||||
import {
|
||||
ToolInput,
|
||||
ToolInputPartial,
|
||||
ToolResult,
|
||||
ToolCancelled,
|
||||
CspMetadata,
|
||||
PermissionsMetadata,
|
||||
McpMethodParams,
|
||||
McpMethodResponse,
|
||||
} from './types';
|
||||
import { AppRenderer } from '@mcp-ui/client';
|
||||
import type {
|
||||
McpUiDisplayMode,
|
||||
McpUiHostContext,
|
||||
McpUiResourceCsp,
|
||||
McpUiResourcePermissions,
|
||||
McpUiSizeChangedNotification,
|
||||
} from '@modelcontextprotocol/ext-apps/app-bridge';
|
||||
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
||||
import { useCallback, useEffect, useMemo, useReducer, useState } from 'react';
|
||||
import { callTool, readResource } from '../../api';
|
||||
import { AppEvents } from '../../constants/events';
|
||||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
import { cn } from '../../utils';
|
||||
import { DEFAULT_IFRAME_HEIGHT } from './utils';
|
||||
import { readResource, callTool } from '../../api';
|
||||
import { errorMessage } from '../../utils/conversionUtils';
|
||||
import { isProtocolSafe, getProtocol } from '../../utils/urlSecurity';
|
||||
import { getProtocol, isProtocolSafe } from '../../utils/urlSecurity';
|
||||
import FlyingBird from '../FlyingBird';
|
||||
import {
|
||||
GooseDisplayMode,
|
||||
SandboxPermissions,
|
||||
McpAppToolCancelled,
|
||||
McpAppToolInput,
|
||||
McpAppToolInputPartial,
|
||||
McpAppToolResult,
|
||||
} from './types';
|
||||
|
||||
const DEFAULT_IFRAME_HEIGHT = 200;
|
||||
|
||||
const AVAILABLE_DISPLAY_MODES: McpUiDisplayMode[] = ['inline'];
|
||||
|
||||
async function fetchMcpAppProxyUrl(csp: McpUiResourceCsp | null): Promise<string | null> {
|
||||
try {
|
||||
const baseUrl = await window.electron.getGoosedHostPort();
|
||||
const secretKey = await window.electron.getSecretKey();
|
||||
|
||||
if (!baseUrl || !secretKey) {
|
||||
console.error('[McpAppRenderer] Failed to get goosed host/port or secret key');
|
||||
return null;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.set('secret', secretKey);
|
||||
|
||||
if (csp?.connectDomains?.length) {
|
||||
params.set('connect_domains', csp.connectDomains.join(','));
|
||||
}
|
||||
if (csp?.resourceDomains?.length) {
|
||||
params.set('resource_domains', csp.resourceDomains.join(','));
|
||||
}
|
||||
if (csp?.frameDomains?.length) {
|
||||
params.set('frame_domains', csp.frameDomains.join(','));
|
||||
}
|
||||
if (csp?.baseUriDomains?.length) {
|
||||
params.set('base_uri_domains', csp.baseUriDomains.join(','));
|
||||
}
|
||||
|
||||
return `${baseUrl}/mcp-app-proxy?${params.toString()}`;
|
||||
} catch (error) {
|
||||
console.error('[McpAppRenderer] Error fetching MCP App Proxy URL:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
interface McpAppRendererProps {
|
||||
resourceUri: string;
|
||||
extensionName: string;
|
||||
sessionId?: string | null;
|
||||
toolInput?: ToolInput;
|
||||
toolInputPartial?: ToolInputPartial;
|
||||
toolResult?: ToolResult;
|
||||
toolCancelled?: ToolCancelled;
|
||||
toolInput?: McpAppToolInput;
|
||||
toolInputPartial?: McpAppToolInputPartial;
|
||||
toolResult?: McpAppToolResult;
|
||||
toolCancelled?: McpAppToolCancelled;
|
||||
append?: (text: string) => void;
|
||||
fullscreen?: boolean;
|
||||
displayMode?: GooseDisplayMode;
|
||||
cachedHtml?: string;
|
||||
}
|
||||
|
||||
interface ResourceData {
|
||||
html: string | null;
|
||||
csp: CspMetadata | null;
|
||||
permissions: PermissionsMetadata | null;
|
||||
interface ResourceMeta {
|
||||
csp: McpUiResourceCsp | null;
|
||||
permissions: SandboxPermissions | null;
|
||||
prefersBorder: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_META: ResourceMeta = { csp: null, permissions: null, prefersBorder: true };
|
||||
|
||||
// Lifecycle: idle → loading_resource → loading_sandbox → ready
|
||||
// Any state can transition to error. The sandbox URL is fetched only once
|
||||
// to prevent iframe recreation (which would cause the app to lose state).
|
||||
type AppState =
|
||||
| { status: 'idle' }
|
||||
| { status: 'loading_resource'; html: string | null; meta: ResourceMeta }
|
||||
| { status: 'loading_sandbox'; html: string; meta: ResourceMeta }
|
||||
| {
|
||||
status: 'ready';
|
||||
html: string;
|
||||
meta: ResourceMeta;
|
||||
sandboxUrl: URL;
|
||||
sandboxCsp: McpUiResourceCsp | null;
|
||||
}
|
||||
| { status: 'error'; message: string; html: string | null; meta: ResourceMeta };
|
||||
|
||||
type AppAction =
|
||||
| { type: 'FETCH_RESOURCE' }
|
||||
| { type: 'RESOURCE_LOADED'; html: string | null; meta: ResourceMeta }
|
||||
| { type: 'RESOURCE_FAILED'; message: string }
|
||||
| { type: 'SANDBOX_READY'; sandboxUrl: string; sandboxCsp: McpUiResourceCsp | null }
|
||||
| { type: 'SANDBOX_FAILED'; message: string }
|
||||
| { type: 'ERROR'; message: string };
|
||||
|
||||
function getMeta(state: AppState): ResourceMeta {
|
||||
return state.status === 'idle' ? DEFAULT_META : state.meta;
|
||||
}
|
||||
|
||||
function getHtml(state: AppState): string | null {
|
||||
return state.status === 'idle' ? null : state.html;
|
||||
}
|
||||
|
||||
function appReducer(state: AppState, action: AppAction): AppState {
|
||||
const meta = getMeta(state);
|
||||
const html = getHtml(state);
|
||||
|
||||
switch (action.type) {
|
||||
case 'FETCH_RESOURCE':
|
||||
return { status: 'loading_resource', html, meta };
|
||||
|
||||
case 'RESOURCE_LOADED':
|
||||
if (!action.html) {
|
||||
return { status: 'loading_resource', html: null, meta: action.meta };
|
||||
}
|
||||
if (state.status === 'ready') {
|
||||
return { ...state, html: action.html, meta: action.meta };
|
||||
}
|
||||
return { status: 'loading_sandbox', html: action.html, meta: action.meta };
|
||||
|
||||
case 'RESOURCE_FAILED':
|
||||
if (html) {
|
||||
if (state.status === 'ready') return state;
|
||||
return { status: 'loading_sandbox', html, meta };
|
||||
}
|
||||
return { status: 'error', message: action.message, html: null, meta };
|
||||
|
||||
case 'SANDBOX_READY':
|
||||
if (!html) return state;
|
||||
return {
|
||||
status: 'ready',
|
||||
html,
|
||||
meta,
|
||||
sandboxUrl: new URL(action.sandboxUrl),
|
||||
sandboxCsp: action.sandboxCsp,
|
||||
};
|
||||
|
||||
case 'SANDBOX_FAILED':
|
||||
return { status: 'error', message: action.message, html, meta };
|
||||
|
||||
case 'ERROR':
|
||||
return { status: 'error', message: action.message, html, meta };
|
||||
}
|
||||
}
|
||||
|
||||
export default function McpAppRenderer({
|
||||
resourceUri,
|
||||
extensionName,
|
||||
@@ -54,25 +182,30 @@ export default function McpAppRenderer({
|
||||
toolResult,
|
||||
toolCancelled,
|
||||
append,
|
||||
fullscreen = false,
|
||||
displayMode = 'inline',
|
||||
cachedHtml,
|
||||
}: McpAppRendererProps) {
|
||||
const [resource, setResource] = useState<ResourceData>({
|
||||
html: cachedHtml || null,
|
||||
csp: null,
|
||||
permissions: null,
|
||||
prefersBorder: true,
|
||||
});
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const isExpandedView = displayMode === 'fullscreen' || displayMode === 'standalone';
|
||||
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
const initialState: AppState = cachedHtml
|
||||
? { status: 'loading_sandbox', html: cachedHtml, meta: DEFAULT_META }
|
||||
: { status: 'idle' };
|
||||
|
||||
const [state, dispatch] = useReducer(appReducer, initialState);
|
||||
const [iframeHeight, setIframeHeight] = useState(DEFAULT_IFRAME_HEIGHT);
|
||||
// null = fluid (100% width), number = explicit width from app
|
||||
const [iframeWidth, setIframeWidth] = useState<number | null>(null);
|
||||
|
||||
// Fetch the resource from the extension to get HTML and metadata (CSP, permissions, etc.).
|
||||
// If cachedHtml is provided we show it immediately; the fetch updates metadata and
|
||||
// replaces HTML only if the server returns different content.
|
||||
useEffect(() => {
|
||||
if (!sessionId) {
|
||||
return;
|
||||
}
|
||||
if (!sessionId) return;
|
||||
|
||||
const fetchResource = async () => {
|
||||
const fetchResourceData = async () => {
|
||||
dispatch({ type: 'FETCH_RESOURCE' });
|
||||
try {
|
||||
const response = await readResource({
|
||||
body: {
|
||||
@@ -84,243 +217,329 @@ export default function McpAppRenderer({
|
||||
|
||||
if (response.data) {
|
||||
const content = response.data;
|
||||
const meta = content._meta as
|
||||
const rawMeta = content._meta as
|
||||
| {
|
||||
ui?: {
|
||||
csp?: CspMetadata;
|
||||
permissions?: PermissionsMetadata;
|
||||
csp?: McpUiResourceCsp;
|
||||
permissions?: McpUiResourcePermissions;
|
||||
prefersBorder?: boolean;
|
||||
};
|
||||
}
|
||||
| undefined;
|
||||
|
||||
if (content.text !== cachedHtml) {
|
||||
setResource({
|
||||
html: content.text,
|
||||
csp: meta?.ui?.csp || null,
|
||||
permissions: meta?.ui?.permissions || null,
|
||||
prefersBorder: meta?.ui?.prefersBorder ?? true,
|
||||
});
|
||||
}
|
||||
dispatch({
|
||||
type: 'RESOURCE_LOADED',
|
||||
html: content.text ?? cachedHtml ?? null,
|
||||
meta: {
|
||||
csp: rawMeta?.ui?.csp || null,
|
||||
// todo: pass permissions to SDK once it supports sendSandboxResourceReady
|
||||
// https://github.com/MCP-UI-Org/mcp-ui/issues/180
|
||||
permissions: null,
|
||||
prefersBorder: rawMeta?.ui?.prefersBorder ?? true,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
if (!cachedHtml) {
|
||||
setError(errorMessage(err, 'Failed to load resource'));
|
||||
} else {
|
||||
console.error('[McpAppRenderer] Error fetching resource:', err);
|
||||
if (cachedHtml) {
|
||||
console.warn('Failed to fetch fresh resource, using cached version:', err);
|
||||
}
|
||||
dispatch({
|
||||
type: 'RESOURCE_FAILED',
|
||||
message: errorMessage(err, 'Failed to load resource'),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
fetchResource();
|
||||
fetchResourceData();
|
||||
}, [resourceUri, extensionName, sessionId, cachedHtml]);
|
||||
|
||||
const handleMcpRequest = useCallback(
|
||||
async (
|
||||
method: string,
|
||||
params: Record<string, unknown> = {},
|
||||
_id?: string | number
|
||||
): Promise<unknown> => {
|
||||
// Methods that require a session
|
||||
const requiresSession = ['tools/call', 'resources/read'];
|
||||
if (requiresSession.includes(method) && !sessionId) {
|
||||
// Create the sandbox proxy URL once we have HTML and metadata.
|
||||
// Fetched only once — recreating the proxy would destroy iframe state.
|
||||
const pendingCsp = state.status === 'loading_sandbox' ? state.meta.csp : null;
|
||||
useEffect(() => {
|
||||
if (state.status !== 'loading_sandbox') return;
|
||||
|
||||
fetchMcpAppProxyUrl(pendingCsp).then((url) => {
|
||||
if (url) {
|
||||
dispatch({ type: 'SANDBOX_READY', sandboxUrl: url, sandboxCsp: pendingCsp });
|
||||
} else {
|
||||
dispatch({ type: 'SANDBOX_FAILED', message: 'Failed to initialize sandbox proxy' });
|
||||
}
|
||||
});
|
||||
}, [state.status, pendingCsp]);
|
||||
|
||||
const handleOpenLink = useCallback(async ({ url }: { url: string }) => {
|
||||
if (isProtocolSafe(url)) {
|
||||
await window.electron.openExternal(url);
|
||||
return { status: 'success' as const };
|
||||
}
|
||||
|
||||
const protocol = getProtocol(url);
|
||||
if (!protocol) {
|
||||
return { status: 'error' as const, message: 'Invalid URL' };
|
||||
}
|
||||
|
||||
const result = await window.electron.showMessageBox({
|
||||
type: 'question',
|
||||
buttons: ['Cancel', 'Open'],
|
||||
defaultId: 0,
|
||||
title: 'Open External Link',
|
||||
message: `Open ${protocol} link?`,
|
||||
detail: `This will open: ${url}`,
|
||||
});
|
||||
|
||||
if (result.response !== 1) {
|
||||
return { status: 'error' as const, message: 'User cancelled' };
|
||||
}
|
||||
|
||||
await window.electron.openExternal(url);
|
||||
return { status: 'success' as const };
|
||||
}, []);
|
||||
|
||||
const handleMessage = useCallback(
|
||||
async ({ content }: { content: Array<{ type: string; text?: string }> }) => {
|
||||
if (!append) {
|
||||
throw new Error('Message handler not available in this context');
|
||||
}
|
||||
if (!Array.isArray(content)) {
|
||||
throw new Error('Invalid message format: content must be an array of ContentBlock');
|
||||
}
|
||||
const textContent = content.find((block) => block.type === 'text');
|
||||
if (!textContent || !textContent.text) {
|
||||
throw new Error('Invalid message format: content must contain a text block');
|
||||
}
|
||||
append(textContent.text);
|
||||
window.dispatchEvent(new CustomEvent(AppEvents.SCROLL_CHAT_TO_BOTTOM));
|
||||
return {};
|
||||
},
|
||||
[append]
|
||||
);
|
||||
|
||||
const handleCallTool = useCallback(
|
||||
async ({
|
||||
name,
|
||||
arguments: args,
|
||||
}: {
|
||||
name: string;
|
||||
arguments?: Record<string, unknown>;
|
||||
}): Promise<CallToolResult> => {
|
||||
if (!sessionId) {
|
||||
throw new Error('Session not initialized for MCP request');
|
||||
}
|
||||
|
||||
switch (method) {
|
||||
case 'ui/open-link': {
|
||||
const { url } = params as McpMethodParams['ui/open-link'];
|
||||
const fullToolName = `${extensionName}__${name}`;
|
||||
const response = await callTool({
|
||||
body: {
|
||||
session_id: sessionId,
|
||||
name: fullToolName,
|
||||
arguments: args || {},
|
||||
},
|
||||
});
|
||||
|
||||
// Safe protocols open directly, unknown protocols require confirmation
|
||||
// Dangerous protocols are blocked by main.ts in the open-external handler
|
||||
if (isProtocolSafe(url)) {
|
||||
await window.electron.openExternal(url);
|
||||
} else {
|
||||
const protocol = getProtocol(url);
|
||||
if (!protocol) {
|
||||
return {
|
||||
status: 'error',
|
||||
message: 'Invalid URL',
|
||||
} as McpMethodResponse['ui/open-link'];
|
||||
}
|
||||
|
||||
const result = await window.electron.showMessageBox({
|
||||
type: 'question',
|
||||
buttons: ['Cancel', 'Open'],
|
||||
defaultId: 0,
|
||||
title: 'Open External Link',
|
||||
message: `Open ${protocol} link?`,
|
||||
detail: `This will open: ${url}`,
|
||||
});
|
||||
if (result.response !== 1) {
|
||||
return {
|
||||
status: 'error',
|
||||
message: 'User cancelled',
|
||||
} as McpMethodResponse['ui/open-link'];
|
||||
}
|
||||
await window.electron.openExternal(url);
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
message: 'Link opened successfully',
|
||||
} satisfies McpMethodResponse['ui/open-link'];
|
||||
}
|
||||
|
||||
case 'ui/message': {
|
||||
const { content } = params as McpMethodParams['ui/message'];
|
||||
if (!append) {
|
||||
throw new Error('Message handler not available in this context');
|
||||
}
|
||||
|
||||
if (!Array.isArray(content)) {
|
||||
throw new Error('Invalid message format: content must be an array of ContentBlock');
|
||||
}
|
||||
|
||||
// Extract first text block from content, ignoring other block types
|
||||
const textContent = content.find((block) => block.type === 'text');
|
||||
if (!textContent) {
|
||||
throw new Error('Invalid message format: content must contain a text block');
|
||||
}
|
||||
|
||||
// MCP Apps can send other content block types, but we only append text blocks for now
|
||||
|
||||
append(textContent.text);
|
||||
window.dispatchEvent(new CustomEvent(AppEvents.SCROLL_CHAT_TO_BOTTOM));
|
||||
return {} satisfies McpMethodResponse['ui/message'];
|
||||
}
|
||||
|
||||
case 'tools/call': {
|
||||
const { name, arguments: args } = params as McpMethodParams['tools/call'];
|
||||
const fullToolName = `${extensionName}__${name}`;
|
||||
const response = await callTool({
|
||||
body: {
|
||||
session_id: sessionId!,
|
||||
name: fullToolName,
|
||||
arguments: args || {},
|
||||
},
|
||||
});
|
||||
return {
|
||||
content: response.data?.content || [],
|
||||
isError: response.data?.is_error || false,
|
||||
structuredContent: (response.data as Record<string, unknown>)?.structured_content as
|
||||
| Record<string, unknown>
|
||||
| undefined,
|
||||
} satisfies McpMethodResponse['tools/call'];
|
||||
}
|
||||
|
||||
case 'resources/read': {
|
||||
const { uri } = params as McpMethodParams['resources/read'];
|
||||
const response = await readResource({
|
||||
body: {
|
||||
session_id: sessionId!,
|
||||
uri,
|
||||
extension_name: extensionName,
|
||||
},
|
||||
});
|
||||
return {
|
||||
contents: response.data ? [response.data] : [],
|
||||
} satisfies McpMethodResponse['resources/read'];
|
||||
}
|
||||
|
||||
case 'notifications/message': {
|
||||
const { level, logger, data } = params as McpMethodParams['notifications/message'];
|
||||
console.log(
|
||||
`[MCP App Notification]${logger ? ` [${logger}]` : ''} ${level || 'info'}:`,
|
||||
data
|
||||
);
|
||||
return {} satisfies McpMethodResponse['notifications/message'];
|
||||
}
|
||||
|
||||
case 'ping':
|
||||
return {} satisfies McpMethodResponse['ping'];
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown method: ${method}`);
|
||||
}
|
||||
// rmcp serializes Content with a `type` discriminator via #[serde(tag = "type")].
|
||||
// Our generated TS types don't reflect this, but the wire format matches CallToolResult.content.
|
||||
return {
|
||||
content: (response.data?.content || []) as unknown as CallToolResult['content'],
|
||||
isError: response.data?.is_error || false,
|
||||
structuredContent: response.data?.structured_content as
|
||||
| { [key: string]: unknown }
|
||||
| undefined,
|
||||
};
|
||||
},
|
||||
[append, sessionId, extensionName]
|
||||
[sessionId, extensionName]
|
||||
);
|
||||
|
||||
const handleSizeChanged = useCallback((height: number, width?: number) => {
|
||||
const newHeight = Math.max(DEFAULT_IFRAME_HEIGHT, height);
|
||||
setIframeHeight(newHeight);
|
||||
setIframeWidth(width ?? null);
|
||||
const handleReadResource = useCallback(
|
||||
async ({ uri }: { uri: string }) => {
|
||||
if (!sessionId) {
|
||||
throw new Error('Session not initialized for MCP request');
|
||||
}
|
||||
const response = await readResource({
|
||||
body: {
|
||||
session_id: sessionId,
|
||||
uri,
|
||||
extension_name: extensionName,
|
||||
},
|
||||
});
|
||||
const data = response.data;
|
||||
if (!data) {
|
||||
return { contents: [] };
|
||||
}
|
||||
return {
|
||||
contents: [{ uri: data.uri || uri, text: data.text, mimeType: data.mimeType || undefined }],
|
||||
};
|
||||
},
|
||||
[sessionId, extensionName]
|
||||
);
|
||||
|
||||
const handleLoggingMessage = useCallback(
|
||||
({ level, logger, data }: { level?: string; logger?: string; data?: unknown }) => {
|
||||
console.log(
|
||||
`[MCP App Notification]${logger ? ` [${logger}]` : ''} ${level || 'info'}:`,
|
||||
data
|
||||
);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
/**
|
||||
* Height: non-positive values are ignored (keeps previous height).
|
||||
* Width: if provided, container uses that width (capped at 100%);
|
||||
* if omitted or non-positive, container is fluid (100%).
|
||||
*/
|
||||
const handleSizeChanged = useCallback(
|
||||
({ height, width }: McpUiSizeChangedNotification['params']) => {
|
||||
if (height !== undefined && height > 0) {
|
||||
setIframeHeight(height);
|
||||
}
|
||||
if (width !== undefined) {
|
||||
setIframeWidth(width > 0 ? width : null);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleError = useCallback((err: Error) => {
|
||||
console.error('[MCP App Error]:', err);
|
||||
dispatch({ type: 'ERROR', message: errorMessage(err) });
|
||||
}, []);
|
||||
|
||||
const { iframeRef, proxyUrl } = useSandboxBridge({
|
||||
resourceHtml: resource.html || '',
|
||||
resourceCsp: resource.csp,
|
||||
resourcePermissions: resource.permissions,
|
||||
resourceUri,
|
||||
toolInput,
|
||||
toolInputPartial,
|
||||
toolResult,
|
||||
toolCancelled,
|
||||
onMcpRequest: handleMcpRequest,
|
||||
onSizeChanged: handleSizeChanged,
|
||||
});
|
||||
const meta = getMeta(state);
|
||||
const html = getHtml(state);
|
||||
|
||||
const readyCsp = state.status === 'ready' ? state.sandboxCsp : null;
|
||||
const mcpUiCsp = useMemo((): McpUiResourceCsp | undefined => {
|
||||
if (!readyCsp) return undefined;
|
||||
return {
|
||||
connectDomains: readyCsp.connectDomains ?? undefined,
|
||||
resourceDomains: readyCsp.resourceDomains ?? undefined,
|
||||
frameDomains: readyCsp.frameDomains ?? undefined,
|
||||
baseUriDomains: readyCsp.baseUriDomains ?? undefined,
|
||||
};
|
||||
}, [readyCsp]);
|
||||
|
||||
const readySandboxUrl = state.status === 'ready' ? state.sandboxUrl : null;
|
||||
const sandboxConfig = useMemo(() => {
|
||||
if (!readySandboxUrl) return null;
|
||||
return {
|
||||
url: readySandboxUrl,
|
||||
permissions: meta.permissions || 'allow-scripts allow-same-origin',
|
||||
csp: mcpUiCsp,
|
||||
};
|
||||
}, [readySandboxUrl, meta.permissions, mcpUiCsp]);
|
||||
|
||||
const hostContext = useMemo((): McpUiHostContext => {
|
||||
const context: McpUiHostContext = {
|
||||
// todo: toolInfo: {}
|
||||
theme: resolvedTheme,
|
||||
// todo: styles: { variables: {}, styles: {} }
|
||||
// 'standalone' is a Goose-specific display mode (dedicated Electron window)
|
||||
// that maps to the spec's inline | fullscreen | pip modes.
|
||||
displayMode: displayMode as McpUiDisplayMode,
|
||||
availableDisplayModes:
|
||||
displayMode === 'standalone' ? [displayMode as McpUiDisplayMode] : AVAILABLE_DISPLAY_MODES,
|
||||
// todo: containerDimensions: {} (depends on displayMode)
|
||||
locale: navigator.language,
|
||||
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
userAgent: navigator.userAgent,
|
||||
platform: 'desktop',
|
||||
deviceCapabilities: {
|
||||
touch: navigator.maxTouchPoints > 0,
|
||||
hover: window.matchMedia('(hover: hover)').matches,
|
||||
},
|
||||
safeAreaInsets: {
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
},
|
||||
};
|
||||
|
||||
return context;
|
||||
}, [resolvedTheme, displayMode]);
|
||||
|
||||
const appToolResult = useMemo((): CallToolResult | undefined => {
|
||||
if (!toolResult) return undefined;
|
||||
// rmcp serializes Content with a `type` discriminator via #[serde(tag = "type")].
|
||||
// Our generated TS types don't reflect this, but the wire format matches CallToolResult.content.
|
||||
return {
|
||||
content: toolResult.content as unknown as CallToolResult['content'],
|
||||
structuredContent: toolResult.structuredContent as { [key: string]: unknown } | undefined,
|
||||
};
|
||||
}, [toolResult]);
|
||||
|
||||
const isToolCancelled = !!toolCancelled;
|
||||
const isError = state.status === 'error';
|
||||
const isReady = state.status === 'ready';
|
||||
|
||||
const renderContent = () => {
|
||||
if (isError) {
|
||||
return (
|
||||
<div className="p-4 text-red-700 dark:text-red-300">
|
||||
Failed to load MCP app: {state.message}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isReady) {
|
||||
return (
|
||||
<div className="relative flex h-full w-full items-center justify-center overflow-hidden rounded bg-black/[0.03] dark:bg-white/[0.03]">
|
||||
<div
|
||||
className="absolute inset-0 animate-shimmer"
|
||||
style={{
|
||||
animationDuration: '2s',
|
||||
background:
|
||||
'linear-gradient(90deg, transparent 0%, rgba(128,128,128,0.08) 40%, rgba(128,128,128,0.12) 50%, rgba(128,128,128,0.08) 60%, transparent 100%)',
|
||||
}}
|
||||
/>
|
||||
<FlyingBird className="relative z-10 scale-200 opacity-30" cycleInterval={120} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!sandboxConfig) return null;
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="p-4 border border-red-500 rounded-lg bg-red-50 dark:bg-red-900/20">
|
||||
<div className="text-red-700 dark:text-red-300">Failed to load MCP app: {error}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (fullscreen) {
|
||||
return proxyUrl ? (
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
src={proxyUrl}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
border: 'none',
|
||||
}}
|
||||
sandbox="allow-scripts allow-same-origin"
|
||||
<AppRenderer
|
||||
sandbox={sandboxConfig}
|
||||
toolName={resourceUri}
|
||||
html={html ?? undefined}
|
||||
toolInput={toolInput?.arguments}
|
||||
toolInputPartial={toolInputPartial ? { arguments: toolInputPartial.arguments } : undefined}
|
||||
toolCancelled={isToolCancelled}
|
||||
hostContext={hostContext}
|
||||
toolResult={appToolResult}
|
||||
onOpenLink={handleOpenLink}
|
||||
onMessage={handleMessage}
|
||||
onCallTool={handleCallTool}
|
||||
onReadResource={handleReadResource}
|
||||
onLoggingMessage={handleLoggingMessage}
|
||||
onSizeChanged={handleSizeChanged}
|
||||
onError={handleError}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
Loading...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const containerClasses = cn(
|
||||
'bg-background-default overflow-hidden',
|
||||
iframeWidth === null && '[&_iframe]:!w-full',
|
||||
isError && 'border border-red-500 rounded-lg bg-red-50 dark:bg-red-900/20',
|
||||
!isError && !isExpandedView && 'mt-6 mb-2',
|
||||
!isError && !isExpandedView && meta.prefersBorder && 'border border-border-default rounded-lg'
|
||||
);
|
||||
|
||||
const containerStyle = isExpandedView
|
||||
? { width: '100%', height: '100%' }
|
||||
: {
|
||||
width: iframeWidth !== null ? `${iframeWidth}px` : '100%',
|
||||
maxWidth: '100%',
|
||||
height: `${iframeHeight || DEFAULT_IFRAME_HEIGHT}px`,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'bg-background-default overflow-hidden',
|
||||
resource.prefersBorder ? 'border border-border-default rounded-lg' : 'my-6'
|
||||
)}
|
||||
>
|
||||
{resource.html && proxyUrl ? (
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
src={proxyUrl}
|
||||
style={{
|
||||
width: iframeWidth ? `${iframeWidth}px` : '100%',
|
||||
maxWidth: '100%',
|
||||
height: `${iframeHeight}px`,
|
||||
border: 'none',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
sandbox="allow-scripts allow-same-origin"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center justify-center p-4" style={{ minHeight: '200px' }}>
|
||||
Loading MCP app...
|
||||
</div>
|
||||
)}
|
||||
<div className={containerClasses} style={containerStyle}>
|
||||
{renderContent()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,107 +1,30 @@
|
||||
export type {
|
||||
CspMetadata,
|
||||
PermissionsMetadata,
|
||||
CallToolResponse as ToolResult,
|
||||
} from '../../api/types.gen';
|
||||
import type {
|
||||
McpUiToolInputNotification,
|
||||
McpUiToolInputPartialNotification,
|
||||
McpUiToolCancelledNotification,
|
||||
McpUiDisplayMode,
|
||||
} from '@modelcontextprotocol/ext-apps/app-bridge';
|
||||
import type { Content } from '../../api';
|
||||
|
||||
export type ContentBlock =
|
||||
| { type: 'text'; text: string }
|
||||
| { type: 'image'; data: string; mimeType: string }
|
||||
| {
|
||||
type: 'resource';
|
||||
resource: { uri: string; mimeType?: string; text?: string; blob?: string };
|
||||
};
|
||||
/**
|
||||
* Space-separated sandbox tokens for iframe permissions.
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox
|
||||
*/
|
||||
export type SandboxPermissions = string;
|
||||
|
||||
export type McpMethodParams = {
|
||||
'ui/open-link': { url: string };
|
||||
'ui/message': { role: 'user'; content: ContentBlock[] };
|
||||
'tools/call': { name: string; arguments?: Record<string, unknown> };
|
||||
'resources/read': { uri: string };
|
||||
'notifications/message': { level?: string; logger?: string; data: unknown };
|
||||
ping: Record<string, never>;
|
||||
export type GooseDisplayMode = McpUiDisplayMode | 'standalone';
|
||||
|
||||
/**
|
||||
* Tool input from the message stream.
|
||||
* McpAppRenderer extracts `.arguments` when passing to the SDK's AppRenderer.
|
||||
*/
|
||||
export type McpAppToolInput = McpUiToolInputNotification['params'];
|
||||
|
||||
export type McpAppToolInputPartial = McpUiToolInputPartialNotification['params'];
|
||||
|
||||
export type McpAppToolCancelled = McpUiToolCancelledNotification['params'];
|
||||
|
||||
export type McpAppToolResult = {
|
||||
content: Content[];
|
||||
structuredContent?: unknown;
|
||||
};
|
||||
|
||||
export type McpMethodResponse = {
|
||||
'ui/open-link': { status: string; message: string };
|
||||
'ui/message': Record<string, never>;
|
||||
'tools/call': {
|
||||
content: unknown[];
|
||||
isError: boolean;
|
||||
structuredContent?: Record<string, unknown>;
|
||||
};
|
||||
'resources/read': { contents: unknown[] };
|
||||
'notifications/message': Record<string, never>;
|
||||
ping: Record<string, never>;
|
||||
};
|
||||
|
||||
export interface JsonRpcRequest {
|
||||
jsonrpc: '2.0';
|
||||
id?: string | number;
|
||||
method: string;
|
||||
params?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface JsonRpcNotification {
|
||||
jsonrpc: '2.0';
|
||||
method: string;
|
||||
params?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface JsonRpcResponse {
|
||||
jsonrpc: '2.0';
|
||||
id: string | number;
|
||||
result?: unknown;
|
||||
error?: {
|
||||
code: number;
|
||||
message: string;
|
||||
data?: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
export type JsonRpcMessage = JsonRpcRequest | JsonRpcNotification | JsonRpcResponse;
|
||||
|
||||
export interface HostContext {
|
||||
toolInfo?: {
|
||||
id?: string | number;
|
||||
tool: {
|
||||
name: string;
|
||||
description?: string;
|
||||
inputSchema?: Record<string, unknown>;
|
||||
};
|
||||
};
|
||||
theme: 'light' | 'dark';
|
||||
displayMode: 'inline' | 'fullscreen' | 'standalone';
|
||||
availableDisplayModes: ('inline' | 'fullscreen' | 'standalone')[];
|
||||
viewport: {
|
||||
width: number;
|
||||
height: number;
|
||||
maxHeight: number;
|
||||
maxWidth: number;
|
||||
};
|
||||
locale: string;
|
||||
timeZone: string;
|
||||
userAgent: string;
|
||||
platform: 'web' | 'desktop' | 'mobile';
|
||||
deviceCapabilities: {
|
||||
touch: boolean;
|
||||
hover: boolean;
|
||||
};
|
||||
safeAreaInsets: {
|
||||
top: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
left: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ToolInput {
|
||||
arguments: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface ToolInputPartial {
|
||||
arguments: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface ToolCancelled {
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
@@ -1,300 +0,0 @@
|
||||
import { useRef, useEffect, useState, useCallback } from 'react';
|
||||
import type {
|
||||
JsonRpcMessage,
|
||||
JsonRpcRequest,
|
||||
JsonRpcNotification,
|
||||
ToolInput,
|
||||
ToolInputPartial,
|
||||
ToolResult,
|
||||
ToolCancelled,
|
||||
HostContext,
|
||||
CspMetadata,
|
||||
PermissionsMetadata,
|
||||
} from './types';
|
||||
import { fetchMcpAppProxyUrl } from './utils';
|
||||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
import packageJson from '../../../package.json';
|
||||
import { errorMessage } from '../../utils/conversionUtils';
|
||||
|
||||
interface SandboxBridgeOptions {
|
||||
resourceHtml: string;
|
||||
resourceCsp: CspMetadata | null;
|
||||
resourcePermissions: PermissionsMetadata | null;
|
||||
resourceUri: string;
|
||||
toolInput?: ToolInput;
|
||||
toolInputPartial?: ToolInputPartial;
|
||||
toolResult?: ToolResult;
|
||||
toolCancelled?: ToolCancelled;
|
||||
onMcpRequest: (
|
||||
method: string,
|
||||
params?: Record<string, unknown>,
|
||||
id?: string | number
|
||||
) => Promise<unknown>;
|
||||
onSizeChanged?: (height: number, width?: number) => void;
|
||||
}
|
||||
|
||||
interface SandboxBridgeResult {
|
||||
iframeRef: React.RefObject<HTMLIFrameElement | null>;
|
||||
proxyUrl: string | null;
|
||||
}
|
||||
|
||||
export function useSandboxBridge(options: SandboxBridgeOptions): SandboxBridgeResult {
|
||||
const {
|
||||
resourceHtml,
|
||||
resourceCsp,
|
||||
resourcePermissions,
|
||||
resourceUri,
|
||||
toolInput,
|
||||
toolInputPartial,
|
||||
toolResult,
|
||||
toolCancelled,
|
||||
onMcpRequest,
|
||||
onSizeChanged,
|
||||
} = options;
|
||||
|
||||
const { resolvedTheme } = useTheme();
|
||||
const iframeRef = useRef<HTMLIFrameElement | null>(null);
|
||||
const isGuestInitializedRef = useRef(false);
|
||||
const [proxyUrl, setProxyUrl] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchMcpAppProxyUrl(resourceCsp).then(setProxyUrl);
|
||||
}, [resourceCsp]);
|
||||
|
||||
// Reset initialization state when resource changes
|
||||
useEffect(() => {
|
||||
isGuestInitializedRef.current = false;
|
||||
}, [resourceUri]);
|
||||
|
||||
const sendToSandbox = useCallback((message: JsonRpcMessage) => {
|
||||
iframeRef.current?.contentWindow?.postMessage(message, '*');
|
||||
}, []);
|
||||
|
||||
const handleJsonRpcMessage = useCallback(
|
||||
async (data: unknown) => {
|
||||
if (!data || typeof data !== 'object') return;
|
||||
|
||||
// Handle notifications (no id)
|
||||
if ('method' in data && !('id' in data)) {
|
||||
const msg = data as JsonRpcNotification;
|
||||
|
||||
switch (msg.method) {
|
||||
case 'ui/notifications/sandbox-ready':
|
||||
sendToSandbox({
|
||||
jsonrpc: '2.0',
|
||||
method: 'ui/notifications/sandbox-resource-ready',
|
||||
params: { html: resourceHtml, csp: resourceCsp, permissions: resourcePermissions },
|
||||
});
|
||||
break;
|
||||
|
||||
case 'ui/notifications/initialized':
|
||||
isGuestInitializedRef.current = true;
|
||||
// Send any pending tool data that arrived before initialization
|
||||
if (toolInput) {
|
||||
sendToSandbox({
|
||||
jsonrpc: '2.0',
|
||||
method: 'ui/notifications/tool-input',
|
||||
params: { arguments: toolInput.arguments },
|
||||
});
|
||||
}
|
||||
if (toolResult) {
|
||||
sendToSandbox({
|
||||
jsonrpc: '2.0',
|
||||
method: 'ui/notifications/tool-result',
|
||||
params: toolResult,
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ui/notifications/size-changed': {
|
||||
const params = msg.params as { height: number; width?: number };
|
||||
onSizeChanged?.(params.height, params.width);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle requests (with id)
|
||||
if ('method' in data && 'id' in data) {
|
||||
const msg = data as JsonRpcRequest;
|
||||
|
||||
try {
|
||||
if (msg.method === 'ui/initialize') {
|
||||
if (msg.id === undefined) return;
|
||||
|
||||
const iframe = iframeRef.current;
|
||||
const hostContext: HostContext = {
|
||||
toolInfo: undefined,
|
||||
theme: resolvedTheme,
|
||||
displayMode: 'inline',
|
||||
availableDisplayModes: ['inline'],
|
||||
viewport: {
|
||||
width: iframe?.clientWidth ?? 0,
|
||||
height: iframe?.clientHeight ?? 0,
|
||||
maxWidth: window.innerWidth,
|
||||
maxHeight: window.innerHeight,
|
||||
},
|
||||
locale: navigator.language,
|
||||
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
userAgent: navigator.userAgent,
|
||||
platform: 'desktop',
|
||||
deviceCapabilities: {
|
||||
touch: 'ontouchstart' in window || navigator.maxTouchPoints > 0,
|
||||
hover: window.matchMedia('(hover: hover)').matches,
|
||||
},
|
||||
safeAreaInsets: { top: 0, right: 0, bottom: 0, left: 0 },
|
||||
};
|
||||
|
||||
sendToSandbox({
|
||||
jsonrpc: '2.0',
|
||||
id: msg.id,
|
||||
result: {
|
||||
protocolVersion: '2025-06-18',
|
||||
hostCapabilities: { links: true, messages: true },
|
||||
hostInfo: {
|
||||
name: packageJson.productName,
|
||||
version: packageJson.version,
|
||||
},
|
||||
hostContext,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await onMcpRequest(msg.method, msg.params, msg.id);
|
||||
if (msg.id !== undefined) {
|
||||
sendToSandbox({ jsonrpc: '2.0', id: msg.id, result });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[Sandbox Bridge] Error handling ${msg.method}:`, error);
|
||||
if (msg.id !== undefined) {
|
||||
sendToSandbox({
|
||||
jsonrpc: '2.0',
|
||||
id: msg.id,
|
||||
error: {
|
||||
code: -32603,
|
||||
message: errorMessage(error),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
resourceHtml,
|
||||
resourceCsp,
|
||||
resourcePermissions,
|
||||
resolvedTheme,
|
||||
sendToSandbox,
|
||||
onMcpRequest,
|
||||
onSizeChanged,
|
||||
toolInput,
|
||||
toolResult,
|
||||
]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const onMessage = (event: MessageEvent) => {
|
||||
if (event.source !== iframeRef.current?.contentWindow) return;
|
||||
handleJsonRpcMessage(event.data);
|
||||
};
|
||||
window.addEventListener('message', onMessage);
|
||||
return () => window.removeEventListener('message', onMessage);
|
||||
}, [handleJsonRpcMessage]);
|
||||
|
||||
// Send tool input notification when it changes
|
||||
useEffect(() => {
|
||||
if (!isGuestInitializedRef.current || !toolInput) return;
|
||||
sendToSandbox({
|
||||
jsonrpc: '2.0',
|
||||
method: 'ui/notifications/tool-input',
|
||||
params: { arguments: toolInput.arguments },
|
||||
});
|
||||
}, [toolInput, sendToSandbox]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isGuestInitializedRef.current || !toolInputPartial) return;
|
||||
sendToSandbox({
|
||||
jsonrpc: '2.0',
|
||||
method: 'ui/notifications/tool-input-partial',
|
||||
params: { arguments: toolInputPartial.arguments },
|
||||
});
|
||||
}, [toolInputPartial, sendToSandbox]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isGuestInitializedRef.current || !toolResult) return;
|
||||
sendToSandbox({
|
||||
jsonrpc: '2.0',
|
||||
method: 'ui/notifications/tool-result',
|
||||
params: toolResult,
|
||||
});
|
||||
}, [toolResult, sendToSandbox]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isGuestInitializedRef.current || !toolCancelled) return;
|
||||
sendToSandbox({
|
||||
jsonrpc: '2.0',
|
||||
method: 'ui/notifications/tool-cancelled',
|
||||
params: toolCancelled.reason ? { reason: toolCancelled.reason } : {},
|
||||
});
|
||||
}, [toolCancelled, sendToSandbox]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isGuestInitializedRef.current) return;
|
||||
sendToSandbox({
|
||||
jsonrpc: '2.0',
|
||||
method: 'ui/notifications/host-context-changed',
|
||||
params: { theme: resolvedTheme },
|
||||
});
|
||||
}, [resolvedTheme, sendToSandbox]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isGuestInitializedRef.current || !iframeRef.current) return;
|
||||
|
||||
const iframe = iframeRef.current;
|
||||
let lastWidth = iframe.clientWidth;
|
||||
let lastHeight = iframe.clientHeight;
|
||||
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
const { width, height } = entries[0].contentRect;
|
||||
const w = Math.round(width);
|
||||
const h = Math.round(height);
|
||||
|
||||
if (w !== lastWidth || h !== lastHeight) {
|
||||
lastWidth = w;
|
||||
lastHeight = h;
|
||||
sendToSandbox({
|
||||
jsonrpc: '2.0',
|
||||
method: 'ui/notifications/host-context-changed',
|
||||
params: {
|
||||
viewport: {
|
||||
width: w,
|
||||
height: h,
|
||||
maxWidth: window.innerWidth,
|
||||
maxHeight: window.innerHeight,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(iframe);
|
||||
return () => observer.disconnect();
|
||||
}, [sendToSandbox]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (isGuestInitializedRef.current) {
|
||||
sendToSandbox({
|
||||
jsonrpc: '2.0',
|
||||
id: Date.now(),
|
||||
method: 'ui/resource-teardown',
|
||||
params: { reason: 'Component unmounting' },
|
||||
});
|
||||
}
|
||||
};
|
||||
}, [sendToSandbox]);
|
||||
|
||||
return { iframeRef, proxyUrl };
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
export const DEFAULT_IFRAME_HEIGHT = 200;
|
||||
|
||||
/**
|
||||
* Fetch the MCP App proxy URL from the Electron backend.
|
||||
* The proxy enforces CSP as a security boundary for sandboxed apps.
|
||||
* TODO(Douwe): make this work better with the generated API rather than poking around
|
||||
*/
|
||||
export async function fetchMcpAppProxyUrl(
|
||||
csp?: {
|
||||
connectDomains?: string[] | null;
|
||||
resourceDomains?: string[] | null;
|
||||
frameDomains?: string[] | null;
|
||||
baseUriDomains?: string[] | null;
|
||||
} | null
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
const baseUrl = await window.electron.getGoosedHostPort();
|
||||
const secretKey = await window.electron.getSecretKey();
|
||||
if (!baseUrl || !secretKey) {
|
||||
console.error('Failed to get goosed host/port or secret key');
|
||||
return null;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.set('secret', secretKey);
|
||||
|
||||
if (csp?.connectDomains?.length) {
|
||||
params.set('connect_domains', csp.connectDomains.join(','));
|
||||
}
|
||||
if (csp?.resourceDomains?.length) {
|
||||
params.set('resource_domains', csp.resourceDomains.join(','));
|
||||
}
|
||||
if (csp?.frameDomains?.length) {
|
||||
params.set('frame_domains', csp.frameDomains.join(','));
|
||||
}
|
||||
if (csp?.baseUriDomains?.length) {
|
||||
params.set('base_uri_domains', csp.baseUriDomains.join(','));
|
||||
}
|
||||
|
||||
return `${baseUrl}/mcp-app-proxy?${params.toString()}`;
|
||||
} catch (error) {
|
||||
console.error('Error fetching MCP App Proxy URL:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AppEvents } from '../constants/events';
|
||||
import { ToolIconWithStatus, ToolCallStatus } from './ToolCallStatusIndicator';
|
||||
import { getToolCallIcon } from '../utils/toolIconMapping';
|
||||
import React, { useEffect, useRef, useState, useMemo } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Button } from './ui/button';
|
||||
import { ToolCallArguments, ToolCallArgumentValue } from './ToolCallArguments';
|
||||
import MarkdownContent from './MarkdownContent';
|
||||
@@ -116,16 +116,13 @@ function McpAppWrapper({
|
||||
? requestWithMeta.toolCall.value.arguments
|
||||
: undefined;
|
||||
|
||||
const toolInput = useMemo(() => ({ arguments: toolArguments || {} }), [toolArguments]);
|
||||
const toolInput = { arguments: toolArguments || {} };
|
||||
|
||||
const toolResult = useMemo(() => {
|
||||
if (!toolResponse) return undefined;
|
||||
const resultWithMeta = toolResponse.toolResult as ToolResultWithMeta;
|
||||
if (resultWithMeta?.status === 'success' && resultWithMeta.value) {
|
||||
return resultWithMeta.value;
|
||||
}
|
||||
return undefined;
|
||||
}, [toolResponse]);
|
||||
const resultWithMeta = toolResponse?.toolResult as ToolResultWithMeta | undefined;
|
||||
const toolResult =
|
||||
resultWithMeta?.status === 'success' && resultWithMeta.value
|
||||
? resultWithMeta.value
|
||||
: undefined;
|
||||
|
||||
if (!resourceUri) return null;
|
||||
if (requestWithMeta.toolCall.status !== 'success') return null;
|
||||
@@ -140,12 +137,6 @@ function McpAppWrapper({
|
||||
sessionId={sessionId}
|
||||
append={append}
|
||||
/>
|
||||
<div className="mt-3 p-4 py-3 border border-border-default rounded-lg bg-background-muted flex items-center">
|
||||
<FlaskConical className="mr-2" size={20} />
|
||||
<div className="text-sm font-sans">
|
||||
MCP Apps are experimental and may change at any time.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ export default function StandaloneAppView() {
|
||||
resourceUri={resourceUri!}
|
||||
extensionName={extensionName!}
|
||||
sessionId={sessionId || null}
|
||||
fullscreen={true}
|
||||
displayMode="standalone"
|
||||
cachedHtml={cachedHtml || undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user