Skip to content

About

No description, website, or topics provided.

Resources

Stars

38 stars

Watchers

2 watching

Forks

Repository files navigation

🏔️ AlpineAI – Multi-Agent Ski Resort Demo

A distributed ski resort system built with Microsoft Agent Framework (MAF), Azure AI Foundry, A2A, Agent Skills over MCP, Voice Live, and Aspire.

An AI-powered ski resort concierge that coordinates weather intelligence, lift traffic, safety evaluation, personalized coaching, web-backed ski research, and voice conversations. Compare remote specialist agents with remote skill instructions and typed MCP tools on the same dashboard.

Compatibility: the skills path uses a pinned historical draft profile of SEP-2640, not the latest discovery contract. As of September 10, 2026, the upstream text is marked Accepted while its PR remains unmerged. Core MCP resources/tools and MAF's experimental progressive-loading API are distinct from that evolving profile. See the profile details.

Architecture

Component Language Role
skiadvisora2a .NET Existing Foundry-hosted orchestrator using specialists as A2A tools
skiadvisorskill Python Native MAF skills and MCP tools advisor; both Responses and A2A hosts use Cosmos-backed history
voiceadvisora2a .NET Voice Live bridge exposing the four specialist A2A agents plus the ski researcher
voiceadvisorskill .NET Voice Live bridge exposing only the skiadvisorskilla2a orchestrator
Compact A2A resources Python/.NET weatheragenta2a, safetyagenta2a, skicoachagenta2a, and lifttrafficagenta2a
Compact skill resources .NET weatherskills, safetyskills, skicoachskills, and lifttrafficskills, exposing instructional skills and twelve typed MCP tools
Ski Researcher Foundry Existing web-backed prompt agent used directly as a tool by both orchestrators
Data Generator Go Continuously generates synthetic resort telemetry
Frontend React/Vite Real-time dashboard with AI chat and voice controls
Desktop MAUI/Avalonia Native Linux/macOS dashboard, streaming chat, and Voice Live

Prerequisites

Install Aspire CLI

Refer to the official Aspire documentation for installation instructions.

The repository pins Aspire 17.0.0-preview.1.26530.1 from the daily channel in aspire.config.json and apphost.cs. The CLI uses that repository-local version; nuget.config maps Aspire packages to the official daily feed. Aspire.Azure.AI.Inference uses 17.0.0-preview.1.26514.11, the latest available daily build of that integration.

To update the repository pins to a newer daily build, run from the repository root:

aspire update --apphost apphost.cs --channel daily --yes --non-interactive --nuget-config-dir .

Setup

1. Clone the repository

git clone https://github.com/tommasodotNET/ski-resort-demo.git
cd ski-resort-demo

2. Configure Azure settings

Create or edit apphost.settings.Development.json in the repository root with your Azure details:

{
    "Azure": {
        "TenantId": "<your-tenant-id>",
        "SubscriptionId": "<your-subscription-id>",
        "AllowResourceGroupCreation": true,
        "ResourceGroup": "<your-resource-group>",
        "Location": "<your-azure-region>",
        "CredentialSource": "AzureCli"
    }
}

Note: The Azure AI Foundry resource must have a chat completion model deployed (e.g., gpt-4.1). The deployment name is configured in the Aspire AppHost.

In Development, the .NET lift-traffic agent skips managed-identity authentication so local requests do not wait on Azure's metadata endpoint. Authenticate with az login; managed identity remains available outside Development.

3. Run the application

From the repository root:

aspire start

This starts both orchestrators, all A2A specialists, their paired .NET MCP skill providers, the shared researcher tool, both voice bridges, frontend, data generator, Foundry resources, and Cosmos DB emulator.

Open the Aspire dashboard (URL shown in terminal output) to see all services, logs, and distributed traces.

The frontend will be available at the URL assigned by Aspire (shown in the dashboard).

Desktop (Linux and macOS)

The native desktop uses the same MAUI-on-Avalonia approach as puff-schedule and budgeting-app: MAUI controls rendered by Avalonia, not an embedded browser. It includes weather measurements and chart, avalanche risk and incidents, grouped slopes/lifts with status and queues, both advisor architectures, streaming chat, new conversations, and microphone/speaker voice conversations with live transcripts. The existing React frontend remains unchanged.

Install the desktop-only SDK and packages once:

bash src/desktop/setup.sh

The pinned .NET 11 preview SDK lives in ignored src/desktop/.dotnet/. Its global.json applies only inside src/desktop; the AppHost and existing .NET 10 services keep their current SDK. No MAUI mobile workloads are needed. Delete that specific .dotnet directory to remove the local SDK.

Start Aspire as usual, then start desktop in the Aspire dashboard. The resource is explicit-start so headless machines and existing web-only development do not launch a GUI or require the desktop SDK. It waits for the frontend and receives its dynamic URL automatically. It is not included in Azure publishing.

Aspire injects ALPINEAI_SERVER_URL; the desktop connects automatically and has no backend address bar or manual connection settings. Starting without that configuration shows an explicit error directing you to launch through Aspire. HTTPS is required except for localhost development. The desktop uses the existing /api, /responses/{a2a|skill}, /ws/voice/{a2a|skill}, and /config.json routes, including the configured polling interval and hosted skills-session continuation. Backend services must still be running; the desktop is not an offline backend.

Voice requires default microphone/output devices and PortAudio: sudo dnf install portaudio (Fedora), sudo apt install libportaudio2 (Ubuntu), or brew install portaudio (macOS). Linux also needs a graphical session and Avalonia's usual X11/font/native graphics libraries. Voice errors, including missing devices or denied microphone access, are shown in the app; the rest of the dashboard works without PortAudio. Use headphones to prevent speaker audio feeding back into the microphone (native capture does not apply the browser's echo cancellation).

Create a self-contained desktop build:

bash src/desktop/publish.sh linux-x64  # also linux-arm64, osx-arm64, osx-x64

Linux output is under src/desktop/artifacts/<rid>/. Published executables also require ALPINEAI_SERVER_URL to be supplied by their orchestrating environment. macOS output is an AlpineAI.app bundle with its microphone permission description. Build/sign it on macOS and grant microphone access when prompted. The script ad-hoc signs on macOS; public distribution still requires Developer ID signing/notarization. PortAudio remains an OS prerequisite. For terminal-based macOS development, grant microphone access to the terminal hosting the app.

Desktop protocol and headless native UI tests:

cd src/desktop
.dotnet/dotnet test AlpineAI.Tests

The native UI tests cover compact dashboard layouts at 640, 768, 1024, and 1380 pixels and repeated A2A/MCP switches, including long streamed and HTTP errors. They check that old chat bubbles are released and retained managed memory stays bounded. Headless checks do not replace testing macOS rendering or audio hardware.

Deploy to Azure

The AppHost deploys supporting services to Azure Container Apps and the two Responses advisors to Foundry hosted agents:

Azure__SubscriptionId="$AZURE_SUBSCRIPTION_ID" \
Azure__Location="$AZURE_LOCATION" \
Azure__ResourceGroup="$AZURE_RESOURCE_GROUP" \
aspire deploy --apphost apphost.cs --non-interactive

For this non-production demo, Cosmos DB allows public network access so the Foundry-hosted skills advisor can reach it without a private endpoint. The AppHost applies the approved SecurityControl=Ignore tag to the Cosmos account to exempt it from time-based resource policies. Authentication remains Microsoft Entra ID; account-key authentication is disabled. Production deployments need their own approved network and policy configuration.

With the pinned Foundry hosting preview, Aspire's Cosmos role assignment targets the generated app identity, not the Foundry hosted agent's runtime identity. After the first deployment, get instance_identity.principal_id from the skiadvisorskill-ha Foundry agent and grant it the native Cosmos data role:

az cosmosdb sql role assignment create \
  --resource-group "$AZURE_RESOURCE_GROUP" \
  --account-name "$COSMOS_ACCOUNT_NAME" \
  --principal-id "$FOUNDRY_SKILLS_AGENT_PRINCIPAL_ID" \
  --role-definition-id 00000000-0000-0000-0000-000000000002 \
  --scope /

This is the Cosmos DB Built-in Data Contributor role at account scope, matching Aspire's generated application roles. The native client needs account metadata during cold startup; a container-only grant was insufficient in hosted testing. The assignment persists across agent version deployments; repeat it if the Foundry agent identity is recreated. Missing this binding produces a Cosmos 403 for Microsoft.DocumentDB/databaseAccounts/readMetadata, even when networking works.

Project Structure

apphost.cs                         # Aspire orchestration (all services wired here)
aspire.config.json                 # Aspire CLI version and launch profiles
apphost.settings.Development.json  # Local Azure configuration (not committed)
src/
├── ski-advisor-a2a/                # skiadvisora2a orchestrator
├── ski-advisor-skill/              # skiadvisorskill + skiadvisorskilla2a surfaces
├── voice-advisor-agent/            # Shared .NET project for both Voice Live resources
├── a2a/                            # Specialist agents exposed over A2A
│   ├── lift-traffic-agent-a2a/     # .NET
│   ├── weather-agent-a2a/          # Python
│   ├── safety-agent-a2a/           # Python
│   └── ski-coach-agent-a2a/        # Python
├── skills/                         # .NET MCP skill providers
│   ├── weather-skills/
│   ├── safety-skills/
│   ├── ski-coach-skills/
│   └── lift-traffic-skills/
├── data-generator/                 # Go data generator
├── frontend/                       # Vite + React + Tailwind dashboard
├── desktop/                        # MAUI/Avalonia desktop, clients, tests, local SDK
├── responses-gateway/              # Frontend gateway used when publishing
├── shared-services/                # .NET shared library (Cosmos, thread store)
└── service-defaults/                # Aspire service defaults

apphost.cs is the source of truth for active services, including publish-only resources. Shared libraries remain alongside the advisors; the ski researcher is a Foundry prompt agent declared in the AppHost, not a separate local MCP project. The lift-traffic A2A project contains only its agent and local function tools; MCP resources and tool hosting live in src/skills/lift-traffic-skills/. The A2A Agent Card's Skills metadata is part of A2A discovery, not MCP hosting.

Configuration

Skills advisor

The skills advisor composes MAF's SkillsProvider / MCPSkillsSource with native MCP functions and request-local FunctionInvocationContext.add_tools. After a skill loads successfully, a small host hook registers every tool from that skill's configured provider for the next model iteration. The model sees their descriptions and schemas before choosing which to invoke. This is the skills architecture's single execution path; no mode switch or Foundry Toolbox is required.

Both Responses and A2A hosting surfaces use the same builder. The frontend's separate A2A-specialist architecture selection remains unchanged. See the skills advisor README for tool loading, approval, lifetime, and local testing.

The shared builder enables multiple tool calls per model response. Independent skill loads and tool calls execute concurrently through MAF's native invocation loop; tools that depend on loaded instructions or earlier results run afterward.

Both skills-advisor hosts use the existing Cosmos DB skillhistory container. The frontend sends a stable conversation ID for skills-chat follow-ups, allowing the Responses host to resume the same agent session and reload earlier turns.

This small demo illustrates a larger-tool-catalog pattern. For genuinely small catalogs, load the configured providers' MCP tools upfront with the standard SDK instead. That simpler composition does not need this demo's skill-to-provider registration hook; it can still load skill instructions on demand.

Data Generator

The data generation speed and drift magnitudes are configurable via src/data-generator/config.json:

{
  "update_interval_seconds": { "min": 5, "max": 10 },
  "weather": { "temperature_drift": 0.1, "wind_speed_drift": 0.5, ... },
  "lifts": { "queue_drift": 3, "status_change_probability": 0.002 },
  ...
}

Frontend

The dashboard polling interval is configurable via src/frontend/public/config.json:

{
  "pollingIntervalMs": 10000
}

Changes are picked up automatically without restarting.

How It Works

  1. Data Generator continuously produces synthetic weather, lift, slope, and safety telemetry via a REST API.

  2. Weather, lift, safety, and coach each have an A2A resource and a paired .NET MCP Agent Skill resource. Skill instructions and typed tools are published remotely rather than embedding domain operations in the skills orchestrator.

  3. Ski Researcher Agent remains an Azure AI Foundry prompt agent with web search. Both orchestrators register it directly as an agent tool, demonstrating that one agent can combine tools and skills.

  4. skiadvisora2a preserves the agent-as-tool architecture. skiadvisorskill uses native MAF skill loading and progressive MCP tool registration. Its model calls registered provider tools directly over MCP. The Foundry ski researcher remains a separate direct agent tool for both orchestrators.

  5. voiceadvisora2a and voiceadvisorskill run the same Voice Live bridge with architecture-specific configuration. The A2A resource registers only the four specialist A2A agents plus the ski researcher; the skill resource registers only skiadvisorskilla2a. The voice conversation ID is preserved through either route and becomes the remote skills-orchestrator session ID when that tool is used.

  6. Frontend displays real-time data panels, provides an AI chat panel, and sends voice sessions to /ws/voice/a2a or /ws/voice/skill according to the selected architecture.

Agent as a Skill over MCP

This sample implements the same four specialist domains as A2A agents and MCP skills:

Agent as a tool Agent as a skill
The advisor sees one function per remote A2A agent The advisor initially sees skill summaries and skill-loading functions
Invoking the function starts a second specialist model run Successful load_skill supplies instructions and triggers registration of the provider's tools
The specialist model chooses and calls its own tools The advisor follows SKILL.md and chooses from the group's full tool descriptions and schemas
A2A returns the specialist's synthesized answer MCP tools/call returns the tool handler's data

Calling this pattern agent as a skill is an architectural mapping: the skill does not contain another agent. The Agent Card's name/description become skill metadata, and the former system prompt becomes an enriched SKILL.md. Specialist tools stay tools, now exposed by MCP handlers over the existing remote domain services. Authentication and transport fields do not become prose. There is no separate specialist model loop, although the advisor uses multiple model iterations.

What the MCP provider publishes

Each .NET provider exposes instructional resources plus typed MCP tools:

skill://index.json
skill://weather/SKILL.md
MCP tools/list: weather_current_conditions, weather_forecast, weather_storm_status

The index is the lightweight discovery layer:

{
  "$schema": "https://schemas.agentskills.io/discovery/0.2.0/schema.json",
  "skills": [{
    "name": "weather",
    "type": "skill-md",
    "description": "Weather intelligence for the ski resort",
    "url": "skill://weather/SKILL.md"
  }]
}

SKILL.md contains domain instructions and explains that the provider's tools become available after the skill loads. Resources carry only discovery and instructions; operational data is returned by tool handlers backed by the existing domain services:

builder.Services.AddMcpServer(options =>
    options.ServerInfo = new() { Name = "weatherskills", Version = "1.0.0" })
    .WithHttpTransport()
    .WithResources<WeatherSkillResources>()
    .WithTools<WeatherTools>();

app.MapMcp("/skillsmcp");

What the Python advisor consumes

SkillsProvider and MCPSkillsSource discover and load the skill documents. MCPStreamableHTTPTool discovers authoritative native functions host-side. A small function-invocation hook associates a successfully loaded skill with its configured provider and registers that provider's whole catalog using MAF's experimental FunctionInvocationContext.add_tools API. The functions retain their descriptions, schemas, native MCP transport, and approval behavior; there is no custom operation dispatcher.

Native progressive-disclosure flow

  1. The SDK reads discovery metadata and the paginated MCP tool catalogs host-side. The initial model context contains skill summaries and generic skill-loading functions, not domain operation schemas or per-tool loaders.
  2. The model calls load_skill({"skill_name":"weather"}); MAF reads the weather SKILL.md.
  3. After the skill read succeeds, the host registers all three weather tools for this run through add_tools. Other providers' tools remain hidden.
  4. On the next model iteration, the model chooses from the weather tools' descriptions and input schemas, for example weather_weather_forecast({"hours":6}).
  5. Native MCP tools/call executes the .NET handler, which reads resort data. The same advisor model uses the result to answer.

The model selects the skill; the host determines the tool group. This skill-to-provider association is application glue, not a feature that SkillsProvider automatically supplies and not a replacement for authorization. Each current provider hosts one skill. Loading it exposes every operation in that provider's catalog, but does not execute them. There are no model-facing per-provider list/load/unload helpers. See the advisor documentation for lifetime and approval details.

The configured providers' tool catalogs are the source of truth; the advisor does not maintain a duplicate tool-name allowlist. A selected skill exposes its provider's full catalog, not only name-matched operations. The current trusted providers expose read-only operations, which retain unattended execution. Native approval_mode="never_require" and the SDK function-calling loop handle that execution automatically. Native SkillsProvider settings disable approval prompts for instruction reads too; there is no approval-resumption bookkeeping.

Registrations belong to the current invocation, not the shared agent. They last through the response stream and disappear when that run ends, including failure or cancellation. Followups start without operation tools and load their skills again; conversation history is separate. MCP connections stay open until app shutdown. The shared native MCP objects do not maintain mutable per-tool-loader state; only their registrations are invocation-local.

Key Technologies

Further Reading

The article and deck compare the two advisor paths using three fresh-conversation pairs at 56f453a: mean elapsed 15.480 s A2A versus 6.348 s skills, with 11,134 versus 13,533 whole-system tokens across the three runs. Skills used three model calls per request; A2A used six, six, and seven including remote specialists. Reused processes, prompt-cache variation, first-use credential probes, live data, and different model-selected work matter. These observations are not a controlled architectural speedup, quality study, or billable-cost ratio.

  • Architecture: component and protocol diagrams.
  • Native MAF migration article: implementation walkthrough and paired measurements using considering weather and waiting time, where should i start?.
  • Presentation source: English Reveal.js deck. Aspire serves it as slides; run npm run build from slides/ to generate the Vite build.

About

No description, website, or topics provided.

Resources

Stars

38 stars

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages