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
35 changes: 34 additions & 1 deletion samples/agent/adk/mcp_app_proxy/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ async def get_calculator_app(tool_context: ToolContext):
"McpApp": {
"content": {"literalString": encoded_html},
"title": {"literalString": "Calculator"},
"allowedTools": ["calculate"],
}
},
}],
Expand All @@ -125,7 +126,39 @@ async def get_calculator_app(tool_context: ToolContext):
logger.error(f"Error fetching calculator app: {e} {traceback.format_exc()}")
return {"error": f"Failed to connect to MCP server or fetch app. Details: {e}"}

tools = [get_calculator_app]
async def calculate_via_mcp(operation: str, a: float, b: float):
"""Calculates via the MCP server's Calculate tool.

Args:
operation: The mathematical operation (e.g. 'add', 'subtract', 'multiply', 'divide').
a: First operand.
b: Second operand.
"""
mcp_server_host = os.getenv("MCP_SERVER_HOST", "localhost")
mcp_server_port = os.getenv("MCP_SERVER_PORT", "8000")
sse_url = f"http://{mcp_server_host}:{mcp_server_port}/sse"

try:
async with sse_client(sse_url) as streams:
async with ClientSession(streams[0], streams[1]) as session:
await session.initialize()

result = await session.call_tool(
"calculate", arguments={"operation": operation, "a": a, "b": b}
)

if (
result.content
and len(result.content) > 0
and hasattr(result.content[0], "text")
):
return result.content[0].text
return "No result text from MCP calculate tool."
except Exception as e:
logger.error(f"Error calling MCP calculate: {e} {traceback.format_exc()}")
return f"Error connecting to MCP server: {e}"

tools = [get_calculator_app, calculate_via_mcp]

agent = McpAppProxyAgent(
base_url=base_url,
Expand Down
7 changes: 5 additions & 2 deletions samples/agent/adk/mcp_app_proxy/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,14 @@
When the user asks for the calculator, you MUST call the `get_calculator_app` tool.

IMPORTANT: Do NOT attempt to construct the JSON manually. The tool `get_calculator_app` handles it automatically.

When the user interacts with the calculator and issues a `calculate` action, you MUST call the `calculate_via_mcp` tool to perform the calculation via the remote MCP server. Return the resulting number directly as text to the user.
"""

WORKFLOW_DESCRIPTION = """
1. **Analyze Request**: User asks for calculator.
2. **Fetch App**: Call `get_calculator_app`.
1. **Analyze Request**:
- If User asks for calculator: Call `get_calculator_app`.
- If User interacts with the calculator (ACTION: calculate): Extract 'operation', 'a', and 'b' from the event context and call `calculate_via_mcp`. Return the result to the user.
"""

UI_DESCRIPTION = """
Expand Down
7 changes: 7 additions & 0 deletions samples/agent/adk/mcp_app_proxy/mcp_app_catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@
"type": "string"
}
}
},
"allowedTools": {
"type": "array",
"description": "List of tool names the app is allowed to call.",
"items": {
"type": "string"
}
}
},
"required": [
Expand Down
9 changes: 9 additions & 0 deletions samples/agent/mcp/mcp-apps-calculcator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,12 @@ npx @modelcontextprotocol/inspector
```

Connect to http://localhost:8000/sse using Transport Type SSE and fetch the resources.

## Available Resources

- **`ui://calculator/app`**: A simple calculator application UI (`text/html;profile=mcp-app`).

## Available Tools

- **`calculate`**: Performs basic arithmetic calculations (`add`, `subtract`, `multiply`, `divide`). It takes `operation`, `a`, and `b` as arguments and returns the result.
- Used by the **"🤖 ="** button in the calculator app to delegate calculations to the MCP server via a `tools/call` request.
50 changes: 47 additions & 3 deletions samples/agent/mcp/mcp-apps-calculcator/apps/calculator.html
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@
button.equals:hover {
background-color: #2da84e;
}
button.mcp-equals {
background-color: #5856d6;
color: white;
grid-column: span 2;
}
button.mcp-equals:hover {
background-color: #4342a1;
}
button.clear {
background-color: #ff3b30;
color: white;
Expand Down Expand Up @@ -110,7 +118,8 @@
<button onclick="appendNumber('1')">1</button>
<button onclick="appendNumber('2')">2</button>
<button onclick="appendNumber('3')">3</button>
<button class="equals" onclick="calculate()">=</button>
<button class="equals" title="Local JavaScript Calculation" onclick="calculateJavascript()">💻 =</button>
<button class="mcp-equals" title="MCP Agent Calculation" onclick="calculateToolCall()">🤖 =</button>
<button onclick="appendNumber('0')">0</button>
<button onclick="appendPoint()">.</button>
</div>
Expand Down Expand Up @@ -145,7 +154,7 @@
function appendOperator(op) {
if (currentInput === '') return;
if (previousInput !== '') {
calculate();
calculateJavascript();
}
operator = op;
previousInput = currentInput;
Expand All @@ -159,7 +168,7 @@
updateDisplay();
}

function calculate() {
function calculateJavascript() {
if (previousInput === '' || currentInput === '') return;
let result;
const prevVal = parseFloat(previousInput);
Expand Down Expand Up @@ -202,6 +211,41 @@
previousInput = '';
}

function calculateToolCall() {
if (previousInput === '' || currentInput === '') return;
const prevVal = parseFloat(previousInput);
const currentVal = parseFloat(currentInput);
if (isNaN(prevVal) || isNaN(currentVal)) return;

let opName = '';
switch (operator) {
case '+': opName = 'add'; break;
case '-': opName = 'subtract'; break;
case '*': opName = 'multiply'; break;
case '/': opName = 'divide'; break;
default: return;
}

sendRequest('tools/call', {
name: 'calculate',
arguments: {
operation: opName,
a: prevVal,
b: currentVal
}
}).then(result => {
console.log("Tool call result:", result);
}).catch(err => {
console.error("Tool call failed:", err);
});

// Reset state locally as per user requirement (UI can forget about it)
currentInput = '';
previousInput = '';
operator = null;
updateDisplay();
}

// MCP Communication
let nextId = 1;

Expand Down
73 changes: 73 additions & 0 deletions samples/agent/mcp/mcp-apps-calculcator/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,79 @@ async def read_resource(uri: str) -> str | bytes:
raise ValueError(f"Resource file not found for uri: {uri}")
raise ValueError(f"Unknown resource: {uri}")

@app.call_tool()
async def handle_call_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]:
if name == "calculate":
try:
operation = arguments.get("operation")
a = float(arguments.get("a"))
b = float(arguments.get("b"))
result = None
if operation == "add":
result = a + b
elif operation == "subtract":
result = a - b
elif operation == "multiply":
result = a * b
elif operation == "divide":
if b == 0:
raise ValueError("Division by zero")
result = a / b
else:
raise ValueError(f"Unknown operation: {operation}")

symbols = {"add": "+", "subtract": "-", "multiply": "*", "divide": "/"}
symbol = symbols.get(operation, "?")
return {
"content": [
{
"type": "text",
"text": f"The calculation of {a} {symbol} {b} is {result}"
}
]
}
except Exception as e:
return {
"content": [
{
"type": "text",
"text": f"Error: {str(e)}"
}
],
"isError": True,
}

raise ValueError(f"Unknown tool: {name}")

@app.list_tools()
async def list_tools() -> list[types.Tool]:
return [
types.Tool(
name="calculate",
title="Calculate",
description="Performs a basic arithmetic calculation.",
inputSchema={
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["add", "subtract", "multiply", "divide"],
"description": "The math operation to perform."
},
"a": {
"type": "number",
"description": "The first operand."
},
"b": {
"type": "number",
"description": "The second operand."
}
},
"required": ["operation", "a", "b"]
}
),
]

if transport == "sse":
from mcp.server.sse import SseServerTransport
from starlette.applications import Starlette
Expand Down
15 changes: 15 additions & 0 deletions samples/client/angular/projects/mcp_calculator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,18 @@ Sample application using the Chat-Canvas component with MCP Calculator Agent.
- `npm start -- mcp_calculator`

6. Open http://localhost:4200/

## Usage

Once the application is running, open `http://localhost:4200/` in your browser. You will see the MCP Calculator interface.

### How to Use

You can perform calculations using standard operations. When computing the expression, there are two different "equals" buttons:

- **Local JavaScript Calculation (💻 =)**: Click this button to evaluate the expression immediately using standard JavaScript in the browser. The result will appear in the calculator display.
- **MCP Agent Tool Call Calculation (🤖 =)**: Click this button to dispatch a tool call to the MCP server. The local display will reset, and the calculation will be handled by the agent asynchronously (the result will appear in the chat conversation).

## Features

- **Client-side Tool Dispatching**: The application supports tool call requests from the MCP client bridge. It maps tool arguments to A2UI Action context and dispatches them to the host agent.
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ const bridge = new AppBridge(
// By default no features will be allowed for the sandbox iframe.
const DEFAULT_SANDBOX_ALLOWED_FEATURES = '';

let innerFrameWindow: Window | null = null;

bridge.oncalltool = async (params: any) => {
// Forward tool calls to parent if needed, or handle locally
// For now, we just log
Expand All @@ -60,30 +62,64 @@ bridge.oncalltool = async (params: any) => {
// 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) {
/**
* Renders the provided HTML inside a nested, sandboxed iframe.
*
* This follows the **Double Iframe Isolation Pattern**:
* 1. The outer iframe (this script) is same-origin with the parent app, preventing
* security exceptions from browser extensions and DevTools.
* 2. This function creates a nested, heavily sandboxed inner iframe to render
* untrusted third-party content.
* 3. By default, no features are allowed for the inner iframe, isolating it from
* the rest of the application.
*
* @param html The raw HTML string to render inside the iframe's srcdoc.
* @param sandbox Allowed features for the sandbox attribute (e.g., 'allow-scripts').
*/
function renderNestedIframe(html: string, sandbox?: string): void {
const content = document.getElementById('content');
if (!content) {
console.error('[Sandbox] Content container not found');
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);
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;

content.innerHTML = '';
content.appendChild(innerFrame);
innerFrameWindow = innerFrame.contentWindow;
}

// Listen for resource ready message
window.addEventListener('message', async (event) => {
if (event.source === window.parent) {
// From Parent (Angular App)
if (event.origin !== window.location.origin) {
return;
}

const data = event.data;
if (data && data.method === SANDBOX_RESOURCE_READY_METHOD) {
const { html, sandbox } = data.params;
if (html) {
renderNestedIframe(html, sandbox);
}
} else {
// TODO: Enable downward communication from parent to inner iframe
// so that the MCP Apps loaded within can receive messages from the host.
console.log('[Sandbox] Received message from parent:', data);
}
} else if (innerFrameWindow && event.source === innerFrameWindow) {
// Allow messages to pass through to the parent only if they come from
// the inner iframe to prevent the parent from receiving messages from
// other iframes on the same page. - This is where the outer-layer iframe
// acts as the guard for the parent
window.parent.postMessage(event.data, window.location.origin);
}
});

Expand Down
Loading
Loading