
MCP Toolbox for Databases now includes official support for the MCP Apps extension.
Written by: Disha Prakash, Software Engineer @ Google
While connecting AI agents to enterprise databases unlocks seamless natural language querying, data exploration often encounters a hurdle. When querying large datasets, the agent usually returns dense, raw JSON payloads. This format lacks the interactive capabilities required for dynamic exploration and complex visualizations.
With the addition of the MCP Apps extension support, we bridge this gap in MCP Toolbox for Databases.
The MCP Apps extension allows the server to return interactive UI components alongside the data.
- In-chat Rendering: When the agent triggers a tool, the chat client detects the attached UI Resource and instantly renders your UI App inside the chat window.
- Data Injection: The client then runs the tool. Instead of loading raw JSON text into the chat, it streams those results into the active app.
- Two-Way Interaction: You can click buttons, filter metrics, or submit forms inside this interactive UI. Those interactions can be communicated back to the Agent and the MCP Server, allowing you to trigger new tool calls.
Tutorial: Building an Account Health Triage Console
Let’s turn a standard database tools into an interactive checklist. We will build a Customer Account Health Triage Console, backed by Cloud SQL Postgres and MCP Toolbox rendered on Claude Desktop. With this app we can:
- Query customer accounts sorted by open support ticket volume.
- Resolve tickets inside the UI widget by making a user initiated tool call.
Step 1: Set Up the Database
Use this query to create sample customers data in Cloud SQL PG
CREATE TABLE customers (id TEXT PRIMARY KEY, name TEXT, segment TEXT, annual_revenue INT, support_tickets INT, nps INT);
INSERT INTO customers VALUES
('C001','Acme Corp','Enterprise',5500000,12,9),
('C002','Globex Corp','Mid-Market',1200000,24,7),
('C003','Initech','Enterprise',8900000,5,10),
('C004','Massive Dynamic','SMB',450000,45,6),
('C005','Hooli','Enterprise',12500000,2,10),
('C006','Pied Piper','Startup',150000,50,8);
Step 2: Create the UI Widget
We will use the official ext-apps SDK for this example.
- Initialize an NPM project
mkdir ui-src && cd ui-src && npm init -y
npm install @modelcontextprotocol/ext-apps
npm install -D vite vite-plugin-singlefile typescript
2. Configure Vite (vite.config.ts)
import { defineConfig } from 'vite';
import { viteSingleFile } from 'vite-plugin-singlefile';
export default defineConfig({ plugins: [viteSingleFile()] });
3. Build the Interface (index.html)
<!DOCTYPE html>
<html><head><style>
body{font-family:system-ui,sans-serif;margin:0;padding:14px;color:#0f172a;font-size:13px}
.header,.row{display:flex;justify-content:space-between;align-items:center}
.header{padding-bottom:10px;border-bottom:2px solid #e2e8f0;margin-bottom:4px}
.row{padding:8px 4px;border-bottom:1px solid #f1f5f9}
.badge{padding:2px 6px;border-radius:4px;font-size:11px;font-weight:600;margin-right:6px}
.risk{background:#fef2f2;color:#991b1b}.ok{background:#f0fdf4;color:#166534}
button{background:#0f172a;color:#fff;border:none;padding:4px 8px;border-radius:4px;font-size:11px;cursor:pointer}
button:disabled{background:#f1f5f9;color:#94a3b8;cursor:default}
</style></head><body>
<div class="header"><strong>Account Triage Console</strong><span id="status" style="color:#64748b;font-size:12px">Syncing...</span></div>
<div id="list"></div>
<script type="module" src="./main.ts"></script>
</body></html>
4. Add Logic (main.ts)
This creates a two-way bridge with Claude Desktop, receiving initial database rows automatically via app.ontoolresult, and executing direct SQL UPDATE statements on button clicks via app.callServerTool().
import { App } from "@modelcontextprotocol/ext-apps";
let accounts: any[] = [];
let app: App | null = null;
function parseContent(content: any[]): any[] {
let parsedRows: any[] = [];
for (const item of content || []) {
if (item.type === "text" && item.text) {
try {
const parsed = JSON.parse(item.text);
if (Array.isArray(parsed)) parsedRows.push(...parsed);
else if (parsed && typeof parsed === "object") parsedRows.push(parsed);
} catch (e) {}
}
}
return parsedRows;
}
async function init() {
app = new App({ name: "Triage", version: "1.0" });
// ontoolresult handles the initial data handoff from the host
app.ontoolresult = (result) => {
if (!result.content) return;
accounts = parseContent(result.content);
document.getElementById("status")!.textContent = "Live";
render();
};
await app.connect();
app.sendSizeChanged({ width: 320, height: 400 });
}
(window as any).resolve = (id: string) => {
if (!app) return;
app.callServerTool({ name: "resolve_account_tickets", arguments: { customer_id: id } }).then(res => {
if (res.content) {
const row = parseContent(res.content)[0];
if (row && row.id) {
const i = accounts.findIndex(a => a.id === row.id);
if (i > -1) { accounts[i] = row; render(); }
}
}
}).catch(console.error);
};
function render() {
document.getElementById("list")!.innerHTML = accounts.map(a => `
<div class="row">
<div><strong>${a.name}</strong> <span style="color:#64748b">(${a.segment})</span></div>
<div style="display:flex;align-items:center">
<span class="badge ${a.support_tickets>=20?'risk':'ok'}">${a.support_tickets} tickets</span>
<button onclick="resolve('${a.id}')" ${!a.support_tickets?'disabled':''}>${!a.support_tickets?'Resolved':'Resolve'}</button>
</div>
</div>`).join("");
}
init().catch(console.error);
5. Bundle and Deploy
npx vite build
cp dist/index.html ../customer-explorer.html
Step 3: Configuration
Define the Toolbox config with this snippet of tools.yaml
kind: source
name: my-cloudsql-db
type: cloud-sql-postgres
project: ${CLOUD_SQL_POSTGRES_PROJECT}
region: ${CLOUD_SQL_POSTGRES_REGION}
instance: ${CLOUD_SQL_POSTGRES_INSTANCE}
database: ${CLOUD_SQL_POSTGRES_DATABASE}
user: ${CLOUD_SQL_POSTGRES_USER}
password: ${CLOUD_SQL_POSTGRES_PASSWORD}
---
# Create a UI resource
kind: resource
name: account-triage-ui
type: file
path: /app/customer-explorer.html
ui: true
prefersBorder: true
description: Account Support Ticket Triage Console
---
kind: tool
name: get_customer_data
type: postgres-sql
source: my-cloudsql-db
description: "Retrieve customer accounts and open support ticket counts."
statement: "SELECT id, name, segment, annual_revenue, support_tickets, nps FROM customers ORDER BY support_tickets DESC;"
ui:
# Attach the UI resource to the tool
resource: account-triage-ui
# [model, app] means BOTH the LLM and the UI widget can execute this tool.
visibility: [model, app]
---
kind: tool
name: resolve_account_tickets
type: postgres-sql
source: my-cloudsql-db
description: "Clear open support tickets for an account."
parameters:
- name: customer_id
type: string
description: "Account ID (e.g., C001)"
statement: "UPDATE customers SET support_tickets = 0 WHERE id = $1 RETURNING *;"
ui:
# Attach the UI resource to the tool
resource: account-triage-ui
# [app] means ONLY the UI widget can execute this tool.
# the LLM cannot invoke this tool
visibility: [app]
Create a Dockerfile in the directory containing customer-explorer.html that we bundled earlier:
# Use the official mcp-toolbox image
FROM us-central1-docker.pkg.dev/database-toolbox/toolbox/toolbox:latest
# Copy the config and UI resource into the container
COPY tools.yaml customer-explorer.html /app/
Step 4: Deploy to Google Cloud Run
Run the following command to build the custom container, substitue your environment variables, and deploy it to Cloud Run.
gcloud run deploy toolbox \
--source . \
--region us-central1 \
--allow-unauthenticated \
--args="--config=/app/tools.yaml","--address=0.0.0.0","--port=8080" \
--set-env-vars="CLOUD_SQL_POSTGRES_PROJECT=your-project-id,CLOUD_SQL_POSTGRES_REGION=you-project-region,CLOUD_SQL_POSTGRES_INSTANCE=your-instance,CLOUD_SQL_POSTGRES_DATABASE=your-database,CLOUD_SQL_POSTGRES_USER=you-user,CLOUD_SQL_POSTGRES_PASSWORD=your-password"
Note: The support for MCP Apps extension in Toolbox is currently available in the MCP Version 2026–07–28 over HTTP.
Step 5: Connect to Claude Desktop
Once Cloud Run is deployed, it will output a URL (e.g., https://toolbox-xyz.a.run.app). Copy this URL, append `/mcp`, and add it as a new connector, under Claude Desktop -> Settings -> Connectors.
Make sure it is connected and you’re able to see the tools.
See It in Action
By bringing the UI directly to the agent, data exploration becomes a native, frictionless part of the chat experience.

Try It Out Today
Support for MCP Apps is available on MCP Toolbox v1.11.0
- Explore the Source: View the documentation on the mcp-toolbox.dev
- Read the Spec: Dive into the Official MCP Apps Guide to learn more about the MCP Spec.
- Build Your Own: See UI App implementation examples in the ext-apps repository. Here’s a list of compatible clients you can use.
Launching MCP Apps in Toolbox was originally published in Google Cloud – Community on Medium, where people are continuing the conversation by highlighting and responding to this story.
Source Credit: https://medium.com/google-cloud/launching-mcp-apps-in-toolbox-0d6e1eed935f?source=rss—-e52cf94d98af—4
