import json
import pandas as pd
from IPython.display import display, Markdown, HTML
from mcp_ski_resort.core import (
extract_text, try_parse_json,
parse_agent_response, parse_analyst_response, parse_search_response,
result_set_to_dataframe,
)
from mcp_ski_resort.mcp_client import (
MCPClient, MCPToolbox, mcp_tools_list, mcp_call, display_mcp_result,
)MCP Client Demo
1. Discover Available Tools
tools = mcp_tools_list()
rows = []
for t in tools:
params = list(t.get('inputSchema', {}).get('properties', {}).keys())
rows.append({
'Tool': t['name'],
'Params': ', '.join(params),
'Description': t.get('description', '')[:80],
})
display(HTML(f'<h3 style="color:#60a5fa;">{len(tools)} MCP Tools Available</h3>'))
display(pd.DataFrame(rows))10 MCP Tools Available
| Tool | Params | Description | |
|---|---|---|---|
| 0 | daily-summary-analyst | message | Executive daily summary analyst - ask natural ... |
| 1 | revenue-analyst | message | Revenue analyst covering ticket sales, rental ... |
| 2 | operations-analyst | message | Operations analyst for lift scans, wait times,... |
| 3 | resort-executive-agent | text | Executive BI partner with access to all resort... |
| 4 | ski-ops-assistant-agent | text | Operations-focused assistant for lift supervis... |
| 5 | feedback-search | query, columns, filter, limit | Semantic search over guest feedback entries. S... |
| 6 | incident-search | query, columns, filter, limit | Semantic search over safety incidents. Search ... |
| 7 | execute-sql | sql | Execute read-only SQL queries against the AM_S... |
| 8 | resort-kpi-summary | p_season | Returns a comprehensive JSON summary of resort... |
| 9 | weather-impact-report | p_start_date, p_end_date | Analyzes how weather conditions impact resort ... |
2. Cortex Agent (CORTEX_AGENT_RUN)
Agents return structured responses with answer text, SQL queries, result sets, and charts.
agent_resp = mcp_call("resort-executive-agent", {
"text": "Give me a quick executive summary of the 2024-2025 season: revenue, visits, and satisfaction."
})
parsed = parse_agent_response(agent_resp)
if parsed.get("answer"):
display(HTML('<h4 style="color:#60a5fa;">Executive Agent: Season Summary</h4>'))
display(Markdown(parsed["answer"][:3000]))
for i, sql in enumerate(parsed.get("sql_queries", [])):
display(HTML(
f'<details><summary style="cursor:pointer;font-weight:600;color:#94a3b8;">SQL {i+1}</summary>'
f'<pre style="padding:12px;background:#0f172a;color:#e2e8f0;border:1px solid #334155;'
f'border-radius:8px;font-size:13px;">{sql}</pre></details>'
))
for i, rs in enumerate(parsed.get("result_sets", [])):
df = result_set_to_dataframe(rs)
if not df.empty:
display(HTML(f'<p style="font-weight:600;color:#d1d5db;">Result {i+1} ({len(df)} rows)</p>'))
display(df.head(10))
else:
display_mcp_result(agent_resp, "Executive Agent")Executive Agent: Season Summary
2024-2025 Season Executive Summary
Season Performance (Nov 1, 2024 – Apr 30, 2025)
Revenue: $7.23M Total
- Food & Beverage: $3.09M (43% of total, 216,944 transactions)
- Ticket Sales: $2.62M (36% of total, 24,041 tickets sold)
- Rentals: $1.51M (21% of total, 27,770 transactions)
Visitation: 103,597 Total Visits
- Unique Visitors: 7,803 guests
- Average Visits per Guest: 13.3 visits
- Pass Holder Share: 76.8% (79,556 visits)
- Day Ticket Visits: 24,041
Guest Satisfaction
- Net Promoter Score (NPS): 7.2 out of 10
- Average Rating: 3.4 out of 5.0
- Total Feedback Collected: 1,553 responses
Key Takeaways: - Strong pass holder loyalty driving repeat visitation with nearly 77% of visits from passholders - F&B has emerged as the largest revenue stream, representing significant ancillary spending opportunity - Guest satisfaction scores are moderate, suggesting opportunity for experience improvements to drive higher ratings and NPS
3. Cortex Analyst (CORTEX_ANALYST_TEXT_TO_SQL)
Natural-language SQL generation over structured data — returns SQL + result sets.
analyst_resp = mcp_call("daily-summary-analyst", {
"message": "How many total visits and unique visitors in the 2024-2025 season?"
})
display(HTML('<h4 style="color:#60a5fa;">Analyst: Season Visitation</h4>'))
display_mcp_result(analyst_resp)Analyst: Season Visitation
This is our interpretation of your question:
How many total visits and unique visitors in the 2024-2025 ski season?
SQL
WITH __fact_pass_usage AS (
SELECT
customer_key,
date_key,
usage_key
FROM AM_SKI_RESORT.MARTS.FACT_PASS_USAGE
), __dim_date AS (
SELECT
date_key,
ski_season
FROM AM_SKI_RESORT.MARTS.DIM_DATE
), __fact_pass_usage AS (
SELECT
customer_key,
date_key,
usage_key
FROM __fact_pass_usage
), __dim_date AS (
SELECT
date_key,
ski_season
FROM __dim_date
)
SELECT
d.ski_season,
COUNT(pu.usage_key) AS total_visits,
COUNT(DISTINCT pu.customer_key) AS unique_visitors
FROM __fact_pass_usage AS pu
LEFT OUTER JOIN __dim_date AS d
ON pu.date_key = d.date_key
WHERE
d.ski_season = '2024-2025'
GROUP BY
d.ski_season
ORDER BY
d.ski_season DESC NULLS LAST
-- Generated by Cortex Analyst (request_id: ba506c71-07e0-4159-9794-0c973d14929c)
;4. Cortex Search (CORTEX_SEARCH_SERVICE_QUERY)
Semantic search over unstructured text — feedback and incidents.
feedback_resp = mcp_call("feedback-search", {
"query": "long lift lines and crowded conditions",
"columns": ["feedback_text", "rating", "sentiment", "category"],
"limit": 10,
})
feedback_df = parse_search_response(feedback_resp)
display(HTML(f'<h4 style="color:#60a5fa;">Feedback Search: "long lift lines" ({len(feedback_df)} results)</h4>'))
if not feedback_df.empty:
display(feedback_df[["feedback_text", "rating", "sentiment", "category"]].head(10))Feedback Search: "long lift lines" (10 results)
| feedback_text | rating | sentiment | category | |
|---|---|---|---|---|
| 0 | Great experience | 5 | positive | Snow_Conditions |
| 1 | Great experience | 4 | positive | Lessons |
| 2 | Great experience | 5 | positive | Parking |
| 3 | Great experience | 4 | positive | Lifts |
| 4 | Great experience | 4 | positive | Food_Service |
| 5 | Great experience | 5 | positive | Lifts |
| 6 | Great experience | 4 | positive | Lifts |
| 7 | Great experience | 4 | positive | Rentals |
| 8 | Great experience | 4 | positive | Food_Service |
| 9 | Great experience | 5 | positive | Facilities |
5. Execute SQL (SYSTEM_EXECUTE_SQL)
Run ad-hoc SQL queries against the resort database.
sql_resp = mcp_call("execute-sql", {
"sql": """
SELECT
l.lift_name,
l.lift_type,
COUNT(*) AS total_scans,
ROUND(AVG(s.wait_time_minutes), 1) AS avg_wait
FROM AM_SKI_RESORT.MARTS.FACT_LIFT_SCANS s
JOIN AM_SKI_RESORT.MARTS.DIM_LIFT l ON s.lift_key = l.lift_key
JOIN AM_SKI_RESORT.MARTS.DIM_DATE d ON s.date_key = d.date_key
WHERE d.ski_season = '2024-2025'
GROUP BY l.lift_name, l.lift_type
ORDER BY avg_wait DESC
LIMIT 10
"""
})
display(HTML('<h4 style="color:#60a5fa;">SQL: Top 10 Lifts by Wait Time (2024-2025)</h4>'))
display_mcp_result(sql_resp)SQL: Top 10 Lifts by Wait Time (2024-2025)
Query ID: 01c3c2a9-0208-8521-004d-de0710f4be8e
Result (10 rows)
| LIFT_NAME | LIFT_TYPE | TOTAL_SCANS | AVG_WAIT | |
|---|---|---|---|---|
| 0 | Mid Mountain | Chair - 4 Pack | 144314 | 4.3 |
| 1 | Family Fun | Chair - 4 Pack | 134416 | 4.1 |
| 2 | Backcountry Access | Chair - 2 Pack | 76932 | 3.6 |
| 3 | Blue Sky | Chair - 4 Pack | 115527 | 3.6 |
| 4 | Cruiser | Chair - 6 Pack | 134463 | 3.5 |
| 5 | Terrain Park Express | Chair - 4 Pack | 86626 | 3.4 |
| 6 | Sunshine | Chair - 4 Pack | 105470 | 3.4 |
| 7 | South Ridge | Chair - 4 Pack | 105050 | 3.4 |
| 8 | Eagle Ridge | Chair - 6 Pack | 124945 | 3.3 |
| 9 | Learners Lift | Chair - 3 Pack | 48021 | 3.2 |
6. KPI Summary (GENERIC UDF)
Call the resort-kpi-summary UDF and render a dashboard card layout.
kpi_resp = mcp_call("resort-kpi-summary", {"p_season": "2024-2025"})
kpi_data = try_parse_json(extract_text(kpi_resp))
if kpi_data:
display(HTML('<h4 style="color:#60a5fa;">KPI Summary: 2024-2025 Season</h4>'))
vis = kpi_data.get("visitation", {})
rev = kpi_data.get("revenue", {})
ops = kpi_data.get("operations", {})
sat = kpi_data.get("satisfaction", {})
safety = kpi_data.get("safety", {})
cards = [
("Total Visits", f"{vis.get('total_visits', 0):,}", ""),
("Unique Visitors", f"{vis.get('unique_visitors', 0):,}", ""),
("Ticket Revenue", f"${rev.get('ticket_revenue', 0):,.0f}", f"{rev.get('ticket_count', 0):,} tickets"),
("Avg Wait", f"{ops.get('avg_wait_minutes', 0):.1f} min", f"Max: {ops.get('max_wait_minutes', 0)} min"),
("NPS Score", f"{sat.get('avg_nps', 0):.1f}", f"Rating: {sat.get('avg_rating', 0):.1f}/5"),
("Incidents", f"{safety.get('total_incidents', 0)}", f"Response: {safety.get('avg_patrol_response_min', 0):.0f} min"),
]
card_html = '<div style="display:flex;flex-wrap:wrap;gap:12px;margin:12px 0;">'
for title, value, subtitle in cards:
card_html += (
f'<div style="flex:1;min-width:150px;padding:16px;background:#1e293b;'
f'border:1px solid #334155;border-radius:12px;text-align:center;">'
f'<div style="font-size:12px;color:#94a3b8;text-transform:uppercase;">{title}</div>'
f'<div style="font-size:28px;font-weight:700;color:#38bdf8;margin:4px 0;">{value}</div>'
f'<div style="font-size:11px;color:#64748b;">{subtitle}</div></div>'
)
card_html += '</div>'
display(HTML(card_html))
else:
display_mcp_result(kpi_resp, "KPI Summary")KPI Summary: 2024-2025 Season
7. Weather Impact (GENERIC UDF)
weather_resp = mcp_call("weather-impact-report", {
"p_start_date": "2024-11-01",
"p_end_date": "2025-04-30",
})
weather_data = try_parse_json(extract_text(weather_resp))
if weather_data:
display(HTML('<h4 style="color:#60a5fa;">Weather Impact: 2024-2025 Season</h4>'))
summary = weather_data.get("weather_summary", {})
powder = weather_data.get("powder_day_impact", {})
conditions = weather_data.get("visitation_by_condition", [])
summary_html = (
f'<div style="padding:12px;background:#0f2918;border:1px solid #166534;border-radius:8px;margin:8px 0;color:#4ade80;">'
f'<b>Weather Summary:</b> {summary.get("total_days", 0)} days analyzed | '
f'Avg Temp: {summary.get("avg_temperature", 0):.0f}\u00b0F | '
f'Total Snowfall: {summary.get("total_snowfall_inches", 0):.0f}" | '
f'Powder Days: {powder.get("powder_day_count", 0)}'
f'</div>'
)
display(HTML(summary_html))
if conditions:
display(pd.DataFrame(conditions))
else:
display_mcp_result(weather_resp, "Weather Impact")Weather Impact: 2024-2025 Season
| avg_daily_visits | avg_hours | snow_condition | visit_days | |
|---|---|---|---|---|
| 0 | 777 | 6.8 | Excellent | 59 |
| 1 | 703 | 6.6 | Good | 62 |
| 2 | 236 | 6.3 | Fair | 60 |
8. Season Report Pipeline
Combine multiple MCP tools into a single executive report.
SEASON = "2024-2025"
report = {}
display(HTML(f'<h2 style="text-align:center;color:#7dd3fc;">Alpine Mountain Ski Resort \u2014 {SEASON} Season Report</h2><hr style="border-color:#334155;"/>'))
print(f"[1/5] Fetching KPIs via resort-kpi-summary...")
kpi = mcp_call("resort-kpi-summary", {"p_season": SEASON})
report["kpi"] = try_parse_json(extract_text(kpi))
print(f"[2/5] Revenue breakdown via revenue-analyst...")
rev = mcp_call("revenue-analyst", {
"message": f"What is the total ticket revenue, rental revenue, and food & beverage revenue for the {SEASON} season?"
})
report["revenue"] = parse_analyst_response(rev)
print(f"[3/5] Operations via operations-analyst...")
ops = mcp_call("operations-analyst", {
"message": f"What are the top 5 busiest lifts by total scans in the {SEASON} season?"
})
report["operations"] = parse_analyst_response(ops)
print(f"[4/5] Guest sentiment via feedback-search...")
neg = mcp_call("feedback-search", {
"query": "worst experience terrible disappointing",
"columns": ["feedback_text", "rating", "sentiment", "category"],
"limit": 5,
})
report["negative_feedback"] = parse_search_response(neg)
print(f"[5/5] Season comparison via resort-executive-agent...")
comparison = mcp_call("resort-executive-agent", {
"text": f"Compare the 2023-2024 season vs {SEASON}: total visits, revenue, NPS. Which season performed better?"
})
report["comparison"] = parse_agent_response(comparison)
print("\nAll data collected!")Alpine Mountain Ski Resort — 2024-2025 Season Report
[1/5] Fetching KPIs via resort-kpi-summary...
[2/5] Revenue breakdown via revenue-analyst...
[3/5] Operations via operations-analyst...
[4/5] Guest sentiment via feedback-search...
[5/5] Season comparison via resort-executive-agent...
All data collected!
display(HTML(f'<h2 style="text-align:center;color:#7dd3fc;">Alpine Mountain \u2014 {SEASON} Executive Report</h2>'))
if report.get("kpi"):
kpi = report["kpi"]
vis = kpi.get("visitation", {})
rev_kpi = kpi.get("revenue", {})
ops_kpi = kpi.get("operations", {})
sat = kpi.get("satisfaction", {})
safety_kpi = kpi.get("safety", {})
metrics = [
("Total Visits", f"{vis.get('total_visits', 0):,}"),
("Unique Visitors", f"{vis.get('unique_visitors', 0):,}"),
("Ticket Revenue", f"${rev_kpi.get('ticket_revenue', 0):,.0f}"),
("Avg Ticket Price", f"${rev_kpi.get('avg_ticket_price', 0):.2f}"),
("Total Lift Scans", f"{ops_kpi.get('total_lift_scans', 0):,}"),
("Avg Wait Time", f"{ops_kpi.get('avg_wait_minutes', 0):.1f} min"),
("NPS Score", f"{sat.get('avg_nps', 0):.1f}"),
("Avg Rating", f"{sat.get('avg_rating', 0):.2f}/5"),
("Safety Incidents", f"{safety_kpi.get('total_incidents', 0)}"),
("Patrol Response", f"{safety_kpi.get('avg_patrol_response_min', 0):.0f} min"),
]
cards_html = '<div style="display:flex;flex-wrap:wrap;gap:10px;margin:16px 0;">'
for title, value in metrics:
cards_html += (
f'<div style="flex:1;min-width:130px;padding:14px;background:#1e293b;'
f'border:1px solid #334155;border-radius:12px;text-align:center;">'
f'<div style="font-size:11px;color:#94a3b8;text-transform:uppercase;letter-spacing:0.5px;">{title}</div>'
f'<div style="font-size:24px;font-weight:700;color:#38bdf8;margin:4px 0;">{value}</div></div>'
)
cards_html += '</div>'
display(HTML(cards_html))
for section, label in [("revenue", "Revenue Breakdown"), ("operations", "Top Lifts")]:
data = report.get(section, {})
if data.get("result_sets"):
display(HTML(f'<h4 style="color:#60a5fa;">{label}</h4>'))
for rs in data["result_sets"]:
df = result_set_to_dataframe(rs)
if not df.empty:
display(df.head(10))
elif data.get("text"):
display(HTML(f'<h4 style="color:#60a5fa;">{label}</h4>'))
display(Markdown(data["text"][:2000]))
if not report.get("negative_feedback", pd.DataFrame()).empty:
display(HTML('<h4 style="color:#f87171;">Worst Guest Feedback</h4>'))
display(report["negative_feedback"].head(5))
comp = report.get("comparison", {})
if comp.get("answer"):
display(HTML('<h4 style="color:#60a5fa;">Season Comparison</h4>'))
display(Markdown(comp["answer"][:3000]))
for rs in comp.get("result_sets", []):
df = result_set_to_dataframe(rs)
if not df.empty:
display(df.head(10))Alpine Mountain — 2024-2025 Executive Report
Worst Guest Feedback
| sentiment | @scores | rating | category | feedback_text | |
|---|---|---|---|---|---|
| 0 | negative | {'text_match': 1.5123662e-07, 'cosine_similari... | 1 | Rentals | Poor experience |
| 1 | negative | {'text_match': 1.5123662e-07, 'cosine_similari... | 3 | Rentals | Poor experience |
| 2 | negative | {'text_match': 1.5123662e-07, 'cosine_similari... | 1 | Lifts | Poor experience |
| 3 | negative | {'text_match': 1.5123662e-07, 'cosine_similari... | 1 | Food_Service | Poor experience |
| 4 | negative | {'text_match': 1.5123662e-07, 'cosine_similari... | 1 | Lifts | Poor experience |
Season Comparison
Season Performance Comparison: 2023-2024 vs. 2024-2025
The 2024-2025 season performed slightly better operationally, with modest gains in visits and revenue, though customer satisfaction saw a marginal decline.
Key Findings
Visits: The 2024-2025 season attracted 103,597 total visits, up 896 visits (+0.87%) from the prior season’s 102,701. Both seasons had comparable data completeness with 181-182 operating days.
Revenue: Total revenue reached $7,227,690 in 2024-2025, an increase of $50,331 (+0.70%) over 2023-2024’s $7,177,359. All three revenue streams grew: - Food & Beverage: $3,094,287 (+$33,458, +1.09%) - Tickets: $2,619,109 (+$6,222, +0.24%) - Rentals: $1,514,294 (+$10,651, +0.71%)
NPS (Customer Satisfaction): Average NPS declined slightly to 7.24 in 2024-2025 from 7.29 in 2023-2024 (-0.05 points, -0.69%), despite receiving more feedback responses (1,553 vs. 1,530).
Strategic Observations
- Revenue per visit declined slightly: Revenue grew at 0.70% while visits grew at 0.87%, indicating lower yield per guest
- F&B drove revenue growth: Food & beverage accounted for 66% of the revenue increase, suggesting strong ancillary spending
- Satisfaction-growth disconnect: The resort grew visits and revenue but at the expense of customer experience, signaling potential capacity or service quality concerns
Bottom Line: 2024-2025 edges out 2023-2024 on volume and revenue metrics, but the NPS decline warrants attention to maintain long-term guest loyalty and pricing power.
9. Using MCPToolbox and MCPClient Directly
MCPToolbox auto-resolves the default session. For full control, construct MCPClient with an explicit URL and headers factory.
toolbox = MCPToolbox()
print(toolbox)
print(f"Tools: {toolbox.tool_names}")
result = toolbox.call("incident-search", {"query": "avalanche near-miss"})
display_mcp_result(result, label="Incident Search")
from mcp_ski_resort.core import default_session, get_headers
s = default_session()
client = MCPClient(
url=f"{s.host}/api/v2/databases/{s.database}/schemas/{s.mcp_schema}/mcp-servers/{s.mcp_server_name}",
headers_factory=get_headers,
)
raw = client.request("tools/list", {})
print(f"Raw JSON-RPC keys: {list(raw.keys())}")MCPToolbox(tools=?, url='https://trb65519.snowflakecomputing.com/api/v2/databases/AM_SKI_RESORT/schemas/MCP_SERVERS/mcp-servers/ski_resort_mcp')
Tools: ['daily-summary-analyst', 'revenue-analyst', 'operations-analyst', 'resort-executive-agent', 'ski-ops-assistant-agent', 'feedback-search', 'incident-search', 'execute-sql', 'resort-kpi-summary', 'weather-impact-report']
Incident Search
| @scores | DESCRIPTION | |
|---|---|---|
| 0 | {'text_match': 0.0, 'cosine_similarity': 0.444... | Lost Skier incident |
| 1 | {'text_match': 0.0, 'cosine_similarity': 0.444... | Lost Skier incident |
| 2 | {'text_match': 0.0, 'cosine_similarity': 0.444... | Lost Skier incident |
| 3 | {'text_match': 0.0, 'cosine_similarity': 0.444... | Lost Skier incident |
| 4 | {'text_match': 0.0, 'cosine_similarity': 0.444... | Lost Skier incident |
| 5 | {'text_match': 0.0, 'cosine_similarity': 0.444... | Lost Skier incident |
| 6 | {'text_match': 0.0, 'cosine_similarity': 0.444... | Lost Skier incident |
| 7 | {'text_match': 0.0, 'cosine_similarity': 0.444... | Lost Skier incident |
| 8 | {'text_match': 0.0, 'cosine_similarity': 0.444... | Lost_Skier incident on 2025-03-16 |
| 9 | {'text_match': 0.0, 'cosine_similarity': 0.444... | Lost_Skier incident on 2025-02-10 |
Raw JSON-RPC keys: ['jsonrpc', 'id', 'result']