Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@ site/
a2a_agents/python/a2ui_agent/src/a2ui/assets/**/*.json
## new agent SDK path
agent_sdks/python/src/a2ui/assets/**/*.json
## Generated JS file from the strictly-typed `sandbox.ts`.
samples/client/angular/projects/orchestrator/public/sandbox_iframe/sandbox.js
7 changes: 5 additions & 2 deletions samples/client/angular/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"serve:ssr:contact": "node dist/contact/server/server.mjs",
"build:renderer": "cd ../../../renderers && for dir in 'web_core' 'markdown/markdown-it'; do (cd \"$dir\" && npm install && npm run build); done",
"serve:agent:restaurant": "cd ../../agent/adk/restaurant_finder && uv run .",
"demo:restaurant": "npm run build:renderer && concurrently -k -n \"AGENT,WEB\" -c \"magenta,blue\" \"npm run serve:agent:restaurant\" \"npm start -- restaurant\""
"demo:restaurant": "npm run build:renderer && concurrently -k -n \"AGENT,WEB\" -c \"magenta,blue\" \"npm run serve:agent:restaurant\" \"npm start -- restaurant\"",
"build:sandbox": "esbuild projects/orchestrator/public/sandbox_iframe/sandbox.ts --bundle --outfile=projects/orchestrator/public/sandbox_iframe/sandbox.js --format=esm --platform=browser"
},
"prettier": {
"printWidth": 100,
Expand All @@ -30,6 +31,7 @@
"private": true,
"dependencies": {
"@a2a-js/sdk": "^0.3.4",
"@modelcontextprotocol/ext-apps": "^1.2.0",
"@a2ui/web_core": "file:../../../renderers/web_core",
"@a2ui/markdown-it": "file:../../../renderers/markdown/markdown-it",
"@angular/cdk": "^20.2.10",
Expand Down Expand Up @@ -83,6 +85,7 @@
},
"workspaces": [
"projects/*",
"../../../renderers/web_core"
"../../../renderers/web_core",
"../../../renderers/markdown/markdown-it"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Sandbox Iframe

This directory contains the `sandbox.html` and its associated resources.

## Purpose

`sandbox.html` is designed to be loaded into an `<iframe>` to provide a secure,
isolated environment for running MCP (Model Context Protocol) applications. It
acts as a bridge between the host application (Orchestrator) and the untrusted
or external MCP apps, managing communication via `postMessage`.

## Development

The logic for the sandbox is written in `sandbox.ts` and must be compiled to
`sandbox.js` to be executable by the browser.

### Build Command

To generate `sandbox.js` from `sandbox.ts`, run the following command from the
`samples/client/angular` directory:

```bash
npm run build:sandbox
```

This ensures that all dependencies (like `@modelcontextprotocol/ext-apps`) are
correctly bundled.
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<!--
Copyright 2025 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MCP App Sandbox</title>
<style>
body, html {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
#content {
width: 100%;
height: 100%;
border: none;
}
</style>
</head>
<body>
<div id="content"></div>
<script type="module" src="sandbox.js"></script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
Copyright 2025 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import {
AppBridge,
PostMessageTransport,
SANDBOX_PROXY_READY_METHOD,
SANDBOX_RESOURCE_READY_METHOD
} from '@modelcontextprotocol/ext-apps/app-bridge';

/**
* Note on "sandbox" terminology:
* The primary functionality of this unit (sandbox.html/ts) is to serve a sandboxed
* environment for the McpApp component.
*
* The sandbox.html is simply a webpage that contains a sandboxed inner iframe.
* The `sandbox` property is an iframe attribute that acts as an allow-list rather than a
* block-list. By default (when it is ''), the iframe will not allow anything, ensuring
* the environment is as vacuum-sealed as possible. Individual features can be enabled
* by setting the `sandbox` property when the host (McpApp component) triggers the
* SANDBOX_RESOURCE_READY_METHOD
*/

// Initialize AppBridge
const bridge = new AppBridge(
null, // No client in sandbox
{ name: 'MCP Sandbox', version: '1.0.0' },
{
serverTools: {},
logging: {}
}
);

// By default no features will be allowed for the sandbox iframe.
const DEFAULT_SANDBOX_ALLOWED_FEATURES = '';

bridge.oncalltool = async (params: any) => {
// Forward tool calls to parent if needed, or handle locally
// For now, we just log
console.log('[Sandbox] Tool call:', params);
// We can also postMessage back to parent manually if bridge doesn't handle it.
// The implementation of forwarding logic would go here
// For now, throw to indicate not implemented in sandbox
throw new Error('Tool execution not supported in sandbox directly');
};

// Notify parent we are ready (standard)
window.parent.postMessage({ method: SANDBOX_PROXY_READY_METHOD }, window.location.origin);

// Listen for resource ready message
window.addEventListener('message', async (event) => {
// Validate the origin of incoming messages
if (event.origin !== window.location.origin) {
return;
}

const data = event.data;
if (data && data.method === SANDBOX_RESOURCE_READY_METHOD) {
const { html, sandbox } = data.params;
const content = document.getElementById('content');
if (html && content) {
// Create an inner iframe with srcdoc to enable Javascript execution if any.
const innerFrame = document.createElement('iframe');
innerFrame.srcdoc = html;
innerFrame.style.width = '100%';
innerFrame.style.height = '100%';
innerFrame.style.border = 'none';
innerFrame.sandbox = sandbox || DEFAULT_SANDBOX_ALLOWED_FEATURES;

// Clear any existing content and inject the new iframe
content.innerHTML = '';
content.appendChild(innerFrame);
}
}
});

// Initialize transport with parent
const transport = new PostMessageTransport(window.parent, window.parent);
await bridge.connect(transport);
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,14 @@ export const DEMO_CATALOG = {
inputBinding('title', () => ('title' in properties && properties['title']) || undefined),
],
},
McpApp: {
type: () => import('./mcp-app').then((r) => r.McpApp),
bindings: ({ properties }) => [
inputBinding(
'content',
() => ('content' in properties && properties['content']) || undefined,
),
inputBinding('title', () => ('title' in properties && properties['title']) || undefined),
],
},
} as Catalog;
Loading
Loading