Skip to content

Commit 9b811e7

Browse files
authored
Use extension bundles for prompt sample (#48)
* add city selection in UI; replaced extension with bundles * add note about preview bundles and package version * updated local dev instructions
1 parent dcf6233 commit 9b811e7

6 files changed

Lines changed: 149 additions & 35 deletions

File tree

src/FunctionsMcpPrompts/README.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,23 @@ This project is a Python Azure Function app that exposes MCP (Model Context Prot
2222

2323
- [Python](https://www.python.org/downloads/) version 3.13 or higher
2424
- [Azure Functions Core Tools](https://learn.microsoft.com/azure/azure-functions/functions-run-local?pivots=programming-language-python#install-the-azure-functions-core-tools) >= `4.8.0`
25-
- `azure-functions` version 2.2.0b2 or greater
26-
- .NET SDK (for building the MCP extension)
25+
- `azure-functions` Python package version **2.2.0b2** or greater (pre-release)
26+
27+
> **Important:** This project uses the **preview extension bundle** (`Microsoft.Azure.Functions.ExtensionBundle.Preview`) configured in `host.json`. The `azure-functions` Python package must also be version `2.2.0b2+` to expose the `mcp_prompt_trigger` decorator.
2728
2829
## Run locally
2930

30-
### 1. Build the MCP extension
31+
### 1. Start Azurite
3132

32-
From this directory (`src/FunctionsMcpPrompts`), build the MCP extension:
33+
An Azure Storage Emulator is needed for the function app to run locally:
3334

3435
```shell
35-
dotnet restore extensions.csproj
36-
dotnet build extensions.csproj
36+
docker run -p 10000:10000 -p 10001:10001 -p 10002:10002 \
37+
mcr.microsoft.com/azure-storage/azurite
3738
```
3839

40+
> **Note**: If using the Azurite VS Code extension, run `Azurite: Start` from the command palette.
41+
3942
### 2. Install Dependencies
4043

4144
Create and activate a virtual environment, then install dependencies:

src/FunctionsMcpPrompts/extensions.csproj

Lines changed: 0 additions & 17 deletions
This file was deleted.

src/FunctionsMcpPrompts/host.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,9 @@
1515
},
1616
"enableLiveMetricsFilters": true
1717
}
18+
},
19+
"extensionBundle": {
20+
"id": "Microsoft.Azure.Functions.ExtensionBundle.Preview",
21+
"version": "[4.41, 5.0.0)"
1822
}
1923
}

src/McpWeatherApp/README.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,27 +30,27 @@ This MCP App provides:
3030

3131
## Local Development
3232

33-
### 1. Build the UI
33+
### 1. Start Azurite
3434

35-
The UI must be bundled before running the function app. From the `src/McpWeatherApp/app` directory:
35+
An Azure Storage Emulator is needed for the function app to run locally:
3636

3737
```bash
38-
npm install
39-
npm run build
38+
docker run -p 10000:10000 -p 10001:10001 -p 10002:10002 \
39+
mcr.microsoft.com/azure-storage/azurite
4040
```
4141

42-
This creates a bundled `app/dist/index.html` file that the function serves.
42+
> **Note**: If using the Azurite VS Code extension, run `Azurite: Start` from the command palette.
4343
44-
### 2. Start Azurite
44+
### 2. Build the UI
4545

46-
An Azure Storage Emulator is needed for the function app to run locally:
46+
The UI must be bundled before running the function app. From the `src/McpWeatherApp/app` directory:
4747

4848
```bash
49-
docker run -p 10000:10000 -p 10001:10001 -p 10002:10002 \
50-
mcr.microsoft.com/azure-storage/azurite
49+
npm install
50+
npm run build
5151
```
5252

53-
> **Note**: If using the Azurite VS Code extension, run `Azurite: Start` from the command palette.
53+
This creates a bundled `app/dist/index.html` file that the function serves.
5454

5555
### 3. Install Python Dependencies
5656

src/McpWeatherApp/app/index.html

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,37 @@
7474
}
7575
.label { color: var(--muted); font-size: 0.85rem; margin: 0 0 6px; }
7676
.value { margin: 0; font-size: 1.1rem; font-weight: 600; }
77+
.city-selector {
78+
margin-bottom: 16px;
79+
}
80+
.city-select {
81+
width: 100%;
82+
background: rgba(255,255,255,0.08);
83+
border: 1px solid rgba(255,255,255,0.12);
84+
border-radius: 10px;
85+
color: var(--text);
86+
padding: 10px 14px;
87+
font-size: 0.95rem;
88+
font-family: inherit;
89+
cursor: pointer;
90+
appearance: none;
91+
-webkit-appearance: none;
92+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%23cbd5e1' viewBox='0 0 16 16'%3E%3Cpath d='M8 11L3 6h10l-5 5z'/%3E%3C/svg%3E");
93+
background-repeat: no-repeat;
94+
background-position: right 14px center;
95+
transition: border-color 0.2s, background 0.2s;
96+
}
97+
.city-select:hover, .city-select:focus {
98+
background: rgba(255,255,255,0.12);
99+
border-color: var(--accent);
100+
outline: none;
101+
}
102+
.city-select option {
103+
background: #1e293b;
104+
color: var(--text);
105+
}
106+
.loading .header .icon { animation: pulse 1.2s infinite; }
107+
@keyframes pulse { 0%,100% { opacity:1 } 50% { opacity:.4 } }
77108
.footer {
78109
margin-top: 12px;
79110
font-size: 0.82rem;
@@ -82,7 +113,8 @@
82113
</style>
83114
</head>
84115
<body>
85-
<div class="widget">
116+
<div class="widget" id="widget">
117+
<div class="city-selector" id="city-selector"></div>
86118
<div class="header">
87119
<div class="icon" id="weather-icon">🌤️</div>
88120
<div>

src/McpWeatherApp/app/weather-app.ts

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,20 @@ import { App } from "@modelcontextprotocol/ext-apps";
33
// DOM element helper
44
const el = (id: string) => document.getElementById(id)!;
55

6+
// Top 10 US cities
7+
const US_CITIES = [
8+
"New York",
9+
"Los Angeles",
10+
"Chicago",
11+
"Houston",
12+
"Phoenix",
13+
"San Francisco",
14+
"Miami",
15+
"Seattle",
16+
"Denver",
17+
"Las Vegas",
18+
];
19+
620
// Weather data interface
721
interface WeatherData {
822
Location?: string;
@@ -119,6 +133,83 @@ function parseToolResultContent(content: Array<{ type: string; text?: string }>
119133
// Create app instance
120134
const app = new App({ name: "Weather Widget", version: "1.0.0" });
121135

136+
// Track active city
137+
let activeCity: string | null = null;
138+
139+
// Build city selector dropdown
140+
function buildCitySelector(): void {
141+
const container = el("city-selector");
142+
const select = document.createElement("select");
143+
select.className = "city-select";
144+
select.id = "city-select";
145+
146+
const placeholder = document.createElement("option");
147+
placeholder.value = "";
148+
placeholder.textContent = "Select a city…";
149+
placeholder.disabled = true;
150+
placeholder.selected = true;
151+
select.appendChild(placeholder);
152+
153+
US_CITIES.forEach((city) => {
154+
const option = document.createElement("option");
155+
option.value = city;
156+
option.textContent = city;
157+
select.appendChild(option);
158+
});
159+
160+
select.addEventListener("change", () => {
161+
if (select.value) selectCity(select.value);
162+
});
163+
164+
container.appendChild(select);
165+
}
166+
167+
// Select a city and fetch weather
168+
async function selectCity(city: string): Promise<void> {
169+
activeCity = city;
170+
171+
// Update dropdown selection
172+
const select = document.getElementById("city-select") as HTMLSelectElement;
173+
if (select) select.value = city;
174+
175+
// Show loading state
176+
el("widget").classList.add("loading");
177+
el("location").textContent = city;
178+
el("condition").textContent = "Loading…";
179+
el("weather-icon").textContent = "🌤️";
180+
el("temperature").textContent = "—";
181+
el("humidity").textContent = "—";
182+
el("wind").textContent = "—";
183+
el("footer").textContent = "Fetching weather…";
184+
185+
try {
186+
const result = await app.callServerTool({
187+
name: "get_weather",
188+
arguments: { location: city },
189+
});
190+
191+
if (result.isError) {
192+
el("condition").textContent = "Error fetching weather";
193+
el("weather-icon").textContent = "⚠️";
194+
} else {
195+
const data = parseToolResultContent(
196+
result.content as Array<{ type: string; text?: string }>
197+
);
198+
if (data) {
199+
render(data);
200+
} else {
201+
el("condition").textContent = "Error parsing weather data";
202+
}
203+
}
204+
} catch (err) {
205+
console.error("callServerTool failed:", err);
206+
el("condition").textContent = "Failed to fetch weather";
207+
el("weather-icon").textContent = "⚠️";
208+
} finally {
209+
el("widget").classList.remove("loading");
210+
}
211+
}
212+
122213
// Register handlers BEFORE connect (events may occur immediately after connect)
123214

124215
// Handle tool input (arguments passed to the tool)
@@ -148,9 +239,10 @@ app.onhostcontextchanged = (ctx) => {
148239

149240
// Connect to host (auto-detects OpenAI vs MCP environment)
150241
(async () => {
242+
buildCitySelector();
151243
await app.connect();
152244

153245
// Apply initial theme from host context
154246
applyTheme(app.getHostContext()?.theme);
155-
el("footer").textContent = "Connected";
247+
el("footer").textContent = "Select a city above to check the weather";
156248
})();

0 commit comments

Comments
 (0)