Skip to content

Commit 08700af

Browse files
committed
feat: Introduce a calculator UI application, integrating it as a server resource and tool, and updating related dependencies.
This calculator MCP App is a precursor to a future sample application that intends to integrate MCP Apps into A2UI. A2UI Custom component that will allow MCP Apps rendering will be a following up. https://screenshot.googleplex.com/3tQRreZ5eQE9qsQ is how the calculator looks.
1 parent b904548 commit 08700af

2 files changed

Lines changed: 272 additions & 0 deletions

File tree

samples/agent/mcp/calculator.html

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>MCP Calculator</title>
7+
<style>
8+
body {
9+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
10+
display: flex;
11+
justify-content: center;
12+
align-items: center;
13+
height: 100vh;
14+
margin: 0;
15+
background-color: #f0f0f0;
16+
}
17+
.calculator {
18+
background: #fff;
19+
padding: 20px;
20+
border-radius: 12px;
21+
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
22+
width: 300px;
23+
}
24+
.display {
25+
width: 100%;
26+
height: 60px;
27+
margin-bottom: 20px;
28+
border: 1px solid #ddd;
29+
border-radius: 8px;
30+
font-size: 2em;
31+
text-align: right;
32+
padding: 10px;
33+
box-sizing: border-box;
34+
overflow-x: auto;
35+
}
36+
.buttons {
37+
display: grid;
38+
grid-template-columns: repeat(4, 1fr);
39+
gap: 10px;
40+
}
41+
button {
42+
padding: 15px;
43+
font-size: 1.2em;
44+
border: none;
45+
border-radius: 8px;
46+
cursor: pointer;
47+
background-color: #e0e0e0;
48+
transition: background-color 0.2s;
49+
}
50+
button:hover {
51+
background-color: #d0d0d0;
52+
}
53+
button.operator {
54+
background-color: #ff9f0a;
55+
color: white;
56+
}
57+
button.operator:hover {
58+
background-color: #e08900;
59+
}
60+
button.equals {
61+
background-color: #34c759;
62+
color: white;
63+
grid-column: span 2;
64+
}
65+
button.equals:hover {
66+
background-color: #2da84e;
67+
}
68+
button.clear {
69+
background-color: #ff3b30;
70+
color: white;
71+
grid-column: span 2;
72+
}
73+
button.clear:hover {
74+
background-color: #d6332a;
75+
}
76+
</style>
77+
</head>
78+
<body>
79+
80+
<div class="calculator">
81+
<div class="display" id="display">0</div>
82+
<div class="buttons">
83+
<button class="clear" onclick="clearDisplay()">AC</button>
84+
<button class="operator" onclick="appendOperator('/')">÷</button>
85+
<button class="operator" onclick="appendOperator('*')">×</button>
86+
<button onclick="appendNumber('7')">7</button>
87+
<button onclick="appendNumber('8')">8</button>
88+
<button onclick="appendNumber('9')">9</button>
89+
<button class="operator" onclick="appendOperator('-')">-</button>
90+
<button onclick="appendNumber('4')">4</button>
91+
<button onclick="appendNumber('5')">5</button>
92+
<button onclick="appendNumber('6')">6</button>
93+
<button class="operator" onclick="appendOperator('+')">+</button>
94+
<button onclick="appendNumber('1')">1</button>
95+
<button onclick="appendNumber('2')">2</button>
96+
<button onclick="appendNumber('3')">3</button>
97+
<button class="equals" onclick="calculate()">=</button>
98+
<button onclick="appendNumber('0')">0</button>
99+
<button onclick="appendPoint()">.</button>
100+
</div>
101+
</div>
102+
103+
<script>
104+
let currentInput = '';
105+
let previousInput = '';
106+
let operator = null;
107+
const displayElement = document.getElementById('display');
108+
109+
function updateDisplay() {
110+
displayElement.textContent = currentInput || '0';
111+
}
112+
113+
function appendNumber(number) {
114+
if (currentInput === '0' && number !== '.') {
115+
currentInput = number;
116+
} else {
117+
currentInput += number;
118+
}
119+
updateDisplay();
120+
}
121+
122+
function appendPoint() {
123+
if (!currentInput.includes('.')) {
124+
currentInput += '.';
125+
updateDisplay();
126+
}
127+
}
128+
129+
function appendOperator(op) {
130+
if (currentInput === '') return;
131+
if (previousInput !== '') {
132+
calculate();
133+
}
134+
operator = op;
135+
previousInput = currentInput;
136+
currentInput = '';
137+
}
138+
139+
function clearDisplay() {
140+
currentInput = '';
141+
previousInput = '';
142+
operator = null;
143+
updateDisplay();
144+
}
145+
146+
function calculate() {
147+
if (previousInput === '' || currentInput === '') return;
148+
let result;
149+
const prev = parseFloat(previousInput);
150+
const current = parseFloat(currentInput);
151+
if (isNaN(prev) || isNaN(current)) return;
152+
153+
switch (operator) {
154+
case '+':
155+
result = prev + current;
156+
break;
157+
case '-':
158+
result = prev - current;
159+
break;
160+
case '*':
161+
result = prev * current;
162+
break;
163+
case '/':
164+
if (current === 0) {
165+
currentInput = 'Error';
166+
operator = null;
167+
previousInput = '';
168+
updateDisplay();
169+
return;
170+
}
171+
result = prev / current;
172+
break;
173+
default:
174+
return;
175+
}
176+
currentInput = result.toString();
177+
updateDisplay();
178+
179+
// Log calculation to host
180+
sendNotification('notifications/message', {
181+
level: 'info',
182+
data: `Calculated: ${prev} ${operator} ${current} = ${result}`
183+
});
184+
185+
operator = null;
186+
previousInput = '';
187+
}
188+
189+
// MCP Communication
190+
let nextId = 1;
191+
192+
function sendRequest(method, params) {
193+
const id = nextId++;
194+
window.parent.postMessage({ jsonrpc: "2.0", id, method, params }, '*');
195+
return new Promise((resolve, reject) => {
196+
const listener = (event) => {
197+
if (event.data?.id === id) {
198+
window.removeEventListener('message', listener);
199+
if (event.data?.result) {
200+
resolve(event.data.result);
201+
} else if (event.data?.error) {
202+
reject(new Error(event.data.error.message || JSON.stringify(event.data.error)));
203+
}
204+
}
205+
};
206+
window.addEventListener('message', listener);
207+
});
208+
}
209+
210+
function sendNotification(method, params) {
211+
window.parent.postMessage({ jsonrpc: "2.0", method, params }, '*');
212+
}
213+
214+
window.addEventListener('message', (event) => {
215+
const data = event.data;
216+
if (data?.method === 'ping') {
217+
// Respond to ping if needed, or just ignore as it might be handled by transport
218+
// The basic-host example sends ping, but maybe we don't need to reply explicitly if using a transport lib
219+
// But here we are raw.
220+
// Actually standard MCP ping is a request, so we should reply.
221+
if (data.id) {
222+
window.parent.postMessage({ jsonrpc: "2.0", id: data.id, result: {} }, '*');
223+
}
224+
}
225+
});
226+
227+
// Initialize
228+
sendRequest("ui/initialize", {
229+
appCapabilities: {
230+
experimental: {},
231+
tools: { listChanged: false },
232+
availableDisplayModes: ["inline"]
233+
}
234+
}).then((result) => {
235+
console.log("Initialized with host:", result);
236+
sendNotification("ui/notifications/initialized", {});
237+
}).catch(err => {
238+
console.error("Initialization failed:", err);
239+
});
240+
241+
</script>
242+
</body>
243+
</html>

samples/agent/mcp/server.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,23 @@ async def handle_call_tool(name: str, arguments: dict[str, Any]) -> dict[str, An
9090

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

93+
@app.list_resources()
94+
async def list_resources() -> list[types.Resource]:
95+
return [
96+
types.Resource(
97+
uri="ui://calculator/app",
98+
name="Calculator App",
99+
mimeType="text/html;profile=mcp-app",
100+
description="A simple calculator application",
101+
)
102+
]
103+
104+
@app.read_resource()
105+
async def read_resource(uri: Any) -> str | bytes:
106+
if str(uri) == "ui://calculator/app":
107+
return (pathlib.Path(__file__).parent / "calculator.html").read_text()
108+
raise ValueError(f"Unknown resource: {uri}")
109+
93110
@app.list_tools()
94111
async def list_tools() -> list[types.Tool]:
95112
return [
@@ -119,6 +136,18 @@ async def list_tools() -> list[types.Tool]:
119136
description="Sends an A2UI error",
120137
inputSchema=a2ui_client_to_server_schema["properties"]["error"],
121138
),
139+
types.Tool(
140+
name="get_calculator_app",
141+
title="Get Calculator App",
142+
description="A simple calculator web app",
143+
inputSchema={"type": "object", "additionalProperties": False},
144+
_meta={
145+
"ui": {
146+
"resourceUri": "ui://calculator/app",
147+
"visibility": ["model", "app"],
148+
}
149+
},
150+
),
122151
]
123152

124153
if transport == "sse":

0 commit comments

Comments
 (0)