chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:58:18 +08:00
commit 6d5d58c1a9
18293 changed files with 3502153 additions and 0 deletions
@@ -0,0 +1,93 @@
/**
* CopilotKit API route with MCP Apps middleware.
* Connects to the travel booking MCP server and enables UI-enabled tools.
*
* Reference: v2.x/apps/react/demo/src/app/api/copilotkit-mcp/[[...slug]]/route.ts
*/
import {
CopilotRuntime,
createCopilotEndpoint,
InMemoryAgentRunner,
} from "@copilotkit/runtime";
import { handle } from "hono/vercel";
import { BuiltInAgent } from "@copilotkit/runtime/v2";
import { MCPAppsMiddleware } from "@ag-ui/mcp-apps-middleware";
// Determine which LLM model to use based on available API keys
const determineModel = () => {
if (process.env.OPENAI_API_KEY?.trim()) {
return "openai/gpt-5.2";
}
if (process.env.ANTHROPIC_API_KEY?.trim()) {
return "anthropic/claude-sonnet-4.5";
}
if (process.env.GOOGLE_API_KEY?.trim()) {
return "google/gemini-2.5-pro";
}
return "openai/gpt-5.2";
};
// Create the agent with multi-app assistant persona and MCP Apps middleware
const agent = new BuiltInAgent({
model: determineModel(),
prompt: `You are an AI assistant with access to 4 interactive apps that render in the chat. Each app provides a rich UI for specific tasks.
## Available Apps
### 1. Airline Booking (search-flights)
Search for flights, select seats, and complete bookings with a full wizard experience.
- Parameters: origin (airport code like JFK, LAX, LHR), destination (airport code), departureDate (YYYY-MM-DD), passengers (1-9), cabinClass (economy/business/first)
- Example: "Book a flight from New York to Los Angeles on January 20th for 2 passengers"
- Helper tools: select-flight, select-seats, book-flight
### 2. Hotel Booking (search-hotels)
Browse hotels, compare rooms, and book accommodations in cities worldwide.
- Parameters: city (Paris, Tokyo, New York, etc.), checkIn (YYYY-MM-DD), checkOut (YYYY-MM-DD), guests (1-6), rooms (1-4)
- Example: "Find a hotel in Paris from January 15 to 18 for 2 guests"
- Helper tools: select-hotel, select-room, book-hotel
### 3. Investment Simulator (create-portfolio)
Create mock investment portfolios with holdings, charts, and trading.
- Parameters: initialBalance (1000-1000000), riskTolerance (conservative/moderate/aggressive), focus (tech/healthcare/diversified/growth/dividend)
- Example: "Create a $10,000 aggressive tech-focused portfolio"
- Helper tools: execute-trade, refresh-prices
### 4. Kanban Board (create-board)
Create task boards with drag-drop cards and columns.
- Parameters: projectName (string), template (blank/software/marketing/personal)
- Example: "Create a kanban board for my software project"
- Helper tools: add-card, update-card, delete-card, move-card
## Guidelines
- When a user's request matches an app, use the appropriate tool to render the interactive UI
- Ask clarifying questions if key parameters are missing
- Each app has helper tools for additional interactions within the UI
- Be helpful and guide users through the interactive features`,
}).use(
new MCPAppsMiddleware({
mcpServers: [
{
type: "http",
url: process.env.MCP_SERVER_URL || "http://localhost:3001/mcp",
},
],
}),
);
// Create CopilotKit runtime
const runtime = new CopilotRuntime({
agents: {
default: agent,
},
runner: new InMemoryAgentRunner(),
});
// Create Hono endpoint
const app = createCopilotEndpoint({
runtime,
basePath: "/api/copilotkit",
});
export const GET = handle(app);
export const POST = handle(app);
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

@@ -0,0 +1,447 @@
/* Plus Jakarta Sans - CopilotKit's brand font */
@import url("https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap");
@import "tailwindcss";
/* =============================================================================
DESIGN SYSTEM - CopilotKit Palette
============================================================================= */
:root {
/* Brand Colors */
--color-lilac: #bec2ff;
--color-lilac-light: #d4d7ff;
--color-lilac-dark: #9599cc;
--color-mint: #85e0ce;
--color-mint-light: #a8e9dc;
--color-mint-dark: #1b936f;
/* Surfaces */
--color-surface: #dedee9;
--color-surface-light: #f7f7f9;
--color-container: #ffffff;
/* Text */
--color-text-primary: #010507;
--color-text-secondary: #57575b;
--color-text-tertiary: #8e8e93;
/* Borders */
--color-border: #dbdbe5;
--color-border-light: #ebebf0;
--color-border-glass: rgba(255, 255, 255, 0.3);
/* Glassmorphism */
--color-glass: rgba(255, 255, 255, 0.7);
--color-glass-subtle: rgba(255, 255, 255, 0.5);
--color-glass-dark: rgba(255, 255, 255, 0.85);
/* Shadows */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md:
0 4px 6px -1px rgba(0, 0, 0, 0.07), 0 2px 4px -1px rgba(0, 0, 0, 0.04);
--shadow-lg:
0 10px 25px -3px rgba(0, 0, 0, 0.08), 0 4px 6px -2px rgba(0, 0, 0, 0.03);
--shadow-glass: 0 4px 30px rgba(0, 0, 0, 0.1);
/* Spacing */
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-8: 32px;
--space-10: 40px;
--space-12: 48px;
/* Radii */
--radius-sm: 6px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-xl: 16px;
--radius-2xl: 24px;
/* Typography */
--font-family: "Plus Jakarta Sans", system-ui, -apple-system, sans-serif;
--font-size-xs: 12px;
--font-size-sm: 14px;
--font-size-base: 16px;
--font-size-lg: 18px;
--font-size-xl: 20px;
--font-size-2xl: 24px;
--font-size-3xl: 32px;
--font-size-4xl: 40px;
color-scheme: light;
}
@theme inline {
--color-background: var(--color-surface-light);
--color-foreground: var(--color-text-primary);
--font-sans: var(--font-family);
}
/* =============================================================================
BASE STYLES
============================================================================= */
body {
font-family: var(--font-family);
background: var(--color-surface-light);
color: var(--color-text-primary);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* =============================================================================
ABSTRACT SHAPES BACKGROUND
============================================================================= */
.abstract-bg {
position: fixed;
inset: 0;
overflow: hidden;
z-index: 0;
background: linear-gradient(
135deg,
var(--color-surface-light) 0%,
var(--color-surface) 100%
);
}
.abstract-bg::before,
.abstract-bg::after {
content: "";
position: absolute;
border-radius: 50%;
filter: blur(80px);
opacity: 0.6;
}
.abstract-bg::before {
width: 600px;
height: 600px;
background: var(--color-lilac);
top: -200px;
right: -100px;
animation: blob1 25s ease-in-out infinite;
}
.abstract-bg::after {
width: 500px;
height: 500px;
background: var(--color-mint);
bottom: -150px;
left: -100px;
animation: blob2 30s ease-in-out infinite;
}
.blob-3 {
position: absolute;
width: 400px;
height: 400px;
background: linear-gradient(
135deg,
var(--color-lilac-light),
var(--color-mint-light)
);
border-radius: 50%;
filter: blur(100px);
opacity: 0.4;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
animation: blob3 35s ease-in-out infinite;
}
@keyframes blob1 {
0%,
100% {
transform: translate(0, 0) scale(1);
}
25% {
transform: translate(-30px, 50px) scale(1.1);
}
50% {
transform: translate(20px, -30px) scale(0.95);
}
75% {
transform: translate(40px, 20px) scale(1.05);
}
}
@keyframes blob2 {
0%,
100% {
transform: translate(0, 0) scale(1);
}
33% {
transform: translate(50px, -40px) scale(1.1);
}
66% {
transform: translate(-30px, 30px) scale(0.9);
}
}
@keyframes blob3 {
0%,
100% {
transform: translate(-50%, -50%) scale(1);
}
25% {
transform: translate(-45%, -55%) scale(1.15);
}
50% {
transform: translate(-55%, -45%) scale(0.9);
}
75% {
transform: translate(-48%, -52%) scale(1.1);
}
}
/* Mobile responsive blobs - reduce size and blur for performance */
@media (max-width: 767px) {
.abstract-bg::before {
width: 300px;
height: 300px;
filter: blur(60px);
}
.abstract-bg::after {
width: 250px;
height: 250px;
filter: blur(50px);
}
.blob-3 {
width: 200px;
height: 200px;
filter: blur(40px);
}
}
/* =============================================================================
GLASSMORPHISM COMPONENTS
============================================================================= */
.glass {
background: var(--color-glass);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid var(--color-border-glass);
border-radius: var(--radius-xl);
box-shadow: var(--shadow-glass);
}
.glass-subtle {
background: var(--color-glass-subtle);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid var(--color-border-glass);
border-radius: var(--radius-lg);
}
.glass-card {
background: var(--color-glass);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid var(--color-border-glass);
border-radius: var(--radius-xl);
box-shadow: var(--shadow-glass);
padding: var(--space-6);
transition:
transform 0.2s ease,
box-shadow 0.2s ease;
}
.glass-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.12);
}
/* =============================================================================
APP CARDS
============================================================================= */
.app-card {
background: var(--color-glass);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid var(--color-border-glass);
border-radius: var(--radius-xl);
padding: var(--space-5);
transition: all 0.3s ease;
cursor: pointer;
position: relative;
overflow: hidden;
}
.app-card::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, var(--color-lilac), var(--color-mint));
opacity: 0;
transition: opacity 0.3s ease;
}
.app-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
}
.app-card:hover::before {
opacity: 1;
}
.app-card-icon {
width: 48px;
height: 48px;
border-radius: var(--radius-lg);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: var(--space-4);
color: white;
}
.app-card-icon.flights {
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
}
.app-card-icon.hotels {
background: linear-gradient(135deg, #f59e0b, #d97706);
}
.app-card-icon.trading {
background: linear-gradient(135deg, #667eea, #764ba2);
}
.app-card-icon.kanban {
background: linear-gradient(135deg, #f093fb, #f5576c);
}
/* =============================================================================
PROMPT PILLS
============================================================================= */
.prompt-pill {
background: var(--color-glass-subtle);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid var(--color-border-glass);
border-radius: var(--radius-2xl);
padding: var(--space-3) var(--space-4);
font-size: var(--font-size-sm);
color: var(--color-text-secondary);
transition: all 0.2s ease;
cursor: pointer;
}
.prompt-pill:hover {
background: var(--color-glass);
color: var(--color-text-primary);
border-color: var(--color-lilac);
transform: translateY(-1px);
}
/* =============================================================================
DOCS BUTTONS
============================================================================= */
.docs-btn {
display: inline-flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-3) var(--space-5);
border-radius: var(--radius-xl);
font-size: var(--font-size-sm);
font-weight: 600;
text-decoration: none;
transition: all 0.2s ease;
cursor: pointer;
border: 1px solid transparent;
font-family: var(--font-family);
}
.docs-btn-primary {
background: linear-gradient(
135deg,
var(--color-lilac-dark),
var(--color-mint-dark)
);
color: white;
box-shadow: var(--shadow-md);
}
.docs-btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(27, 147, 111, 0.35);
filter: brightness(1.05);
}
.docs-btn-secondary {
background: var(--color-glass);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid var(--color-border-glass);
color: var(--color-text-primary);
}
.docs-btn-secondary:hover {
background: var(--color-glass-dark);
border-color: var(--color-lilac);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
/* =============================================================================
COPILOTKIT SIDEBAR OVERRIDES
============================================================================= */
/* Sidebar container */
[data-copilotkit-sidebar] {
--copilot-kit-background-color: rgba(247, 247, 249, 0.95) !important;
--copilot-kit-secondary-color: rgba(255, 255, 255, 0.7) !important;
--copilot-kit-primary-color: var(--color-lilac) !important;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-left: 1px solid var(--color-border-glass) !important;
}
/* Message bubbles */
[data-copilotkit-sidebar] .copilotkit-message {
font-family: var(--font-family);
}
/* Input area */
[data-copilotkit-sidebar] textarea,
[data-copilotkit-sidebar] input {
font-family: var(--font-family);
border-radius: var(--radius-lg);
}
/* =============================================================================
UTILITY CLASSES
============================================================================= */
.text-gradient {
background: linear-gradient(
135deg,
var(--color-lilac-dark),
var(--color-mint-dark)
);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.border-gradient {
border: 2px solid transparent;
background:
linear-gradient(var(--color-container), var(--color-container)) padding-box,
linear-gradient(135deg, var(--color-lilac), var(--color-mint)) border-box;
}
@@ -0,0 +1,35 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "@copilotkit/react-core/v2/styles.css";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "AI Fitness Coach - MCP Apps Demo",
description: "Interactive workout demo built with CopilotKit and MCP Apps",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
);
}
@@ -0,0 +1,343 @@
"use client";
import {
CopilotKitProvider,
CopilotSidebar,
CopilotPopup,
useAgent,
useCopilotKit,
useCopilotChatConfiguration,
} from "@copilotkit/react-core/v2";
import { useMediaQuery } from "@/hooks/use-media-query";
import { randomUUID, DEFAULT_AGENT_ID } from "@copilotkit/shared";
import { useCallback } from "react";
export const dynamic = "force-dynamic";
// Lucide-style SVG icons (inline for reliability)
const Icons = {
plane: (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M17.8 19.2 16 11l3.5-3.5C21 6 21.5 4 21 3c-1-.5-3 0-4.5 1.5L13 8 4.8 6.2c-.5-.1-.9.1-1.1.5l-.3.5c-.2.5-.1 1 .3 1.3L9 12l-2 3H4l-1 1 3 2 2 3 1-1v-3l3-2 3.5 5.3c.3.4.8.5 1.3.3l.5-.2c.4-.3.6-.7.5-1.2z" />
</svg>
),
building: (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect width="16" height="20" x="4" y="2" rx="2" ry="2" />
<path d="M9 22v-4h6v4" />
<path d="M8 6h.01" />
<path d="M16 6h.01" />
<path d="M12 6h.01" />
<path d="M12 10h.01" />
<path d="M12 14h.01" />
<path d="M16 10h.01" />
<path d="M16 14h.01" />
<path d="M8 10h.01" />
<path d="M8 14h.01" />
</svg>
),
trendingUp: (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="22 7 13.5 15.5 8.5 10.5 2 17" />
<polyline points="16 7 22 7 22 13" />
</svg>
),
layoutGrid: (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect width="7" height="7" x="3" y="3" rx="1" />
<rect width="7" height="7" x="14" y="3" rx="1" />
<rect width="7" height="7" x="14" y="14" rx="1" />
<rect width="7" height="7" x="3" y="14" rx="1" />
</svg>
),
sparkles: (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z" />
<path d="M20 3v4" />
<path d="M22 5h-4" />
<path d="M4 17v2" />
<path d="M5 18H3" />
</svg>
),
};
const apps = [
{
id: "flights",
name: "Airline Booking",
description:
"Search flights, select seats, and complete bookings with a full wizard experience",
icon: Icons.plane,
iconClass: "flights",
prompts: [
"Book a flight from New York to Los Angeles on January 20th for 2 passengers",
"Find flights from London to Paris next week",
],
},
{
id: "hotels",
name: "Hotel Booking",
description:
"Browse hotels, compare rooms, and book accommodations in cities worldwide",
icon: Icons.building,
iconClass: "hotels",
prompts: [
"Find a hotel in Paris from January 15 to 18 for 2 guests",
"Search for hotels in Tokyo for 3 nights",
],
},
{
id: "trading",
name: "Investment Simulator",
description:
"Build portfolios, execute trades, and track performance with live charts",
icon: Icons.trendingUp,
iconClass: "trading",
prompts: [
"Create a $10,000 tech-focused portfolio",
"Build a conservative dividend portfolio",
],
},
{
id: "kanban",
name: "Kanban Board",
description:
"Manage projects with drag-drop cards, columns, and task tracking",
icon: Icons.layoutGrid,
iconClass: "kanban",
prompts: [
"Create a kanban board for my software project",
"Set up a marketing campaign board",
],
},
];
export default function MCPAppsDemo() {
return (
<CopilotKitProvider runtimeUrl="/api/copilotkit" showDevConsole="auto">
<AppLayout />
</CopilotKitProvider>
);
}
function AppLayout() {
const { agent } = useAgent({ agentId: DEFAULT_AGENT_ID });
const { copilotkit } = useCopilotKit();
const isDesktop = useMediaQuery("(min-width: 768px)");
const config = useCopilotChatConfiguration();
// Send a message to the chat and run the agent
const sendMessage = useCallback(
async (message: string) => {
config?.setModalOpen(true);
agent.addMessage({
id: randomUUID(),
role: "user",
content: message,
});
try {
await copilotkit.runAgent({ agent });
} catch (error) {
console.error("Failed to run agent:", error);
}
},
[agent, copilotkit, config],
);
return (
<div className="relative min-h-screen overflow-hidden">
{/* Animated Abstract Background */}
<div className="abstract-bg">
<div className="blob-3" />
</div>
{/* Main Content */}
<main className="relative z-10 mx-auto flex w-full max-w-5xl flex-col gap-8 px-4 py-6 md:px-6 md:py-12">
{/* Hero Section */}
<section className="text-center space-y-6">
<div className="inline-flex items-center gap-2 glass-subtle px-4 py-2 rounded-full text-sm text-[var(--color-text-secondary)]">
{Icons.sparkles}
<span>MCP Apps Demo</span>
</div>
<h1 className="text-4xl md:text-5xl font-bold tracking-tight text-[var(--color-text-primary)]">
Interactive AI Apps in Chat
</h1>
<p className="max-w-2xl mx-auto text-lg text-[var(--color-text-secondary)]">
Rich UI components that render directly in the chat sidebar. Powered
by the MCP Apps Extension (SEP-1865) with bidirectional
communication.
</p>
{/* Docs buttons */}
<div className="flex flex-wrap items-center justify-center gap-3 pt-2">
<a
href="https://go.copilotkit.ai/mcp-apps"
target="_blank"
rel="noopener noreferrer"
className="docs-btn docs-btn-primary"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z" />
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z" />
</svg>
Read more
</a>
<a
href="https://go.copilotkit.ai/mcp-apps-docs"
target="_blank"
rel="noopener noreferrer"
className="docs-btn docs-btn-secondary"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M12 2L2 7l10 5 10-5-10-5z" />
<path d="M2 17l10 5 10-5" />
</svg>
Docs
</a>
</div>
</section>
{/* App Cards Grid */}
<section className="grid gap-6 md:grid-cols-2">
{apps.map((app) => (
<div key={app.id} className="app-card">
<div className={`app-card-icon ${app.iconClass}`}>{app.icon}</div>
<h3 className="text-lg font-semibold text-[var(--color-text-primary)] mb-2">
{app.name}
</h3>
<p className="text-sm text-[var(--color-text-secondary)] mb-4">
{app.description}
</p>
<div className="flex flex-wrap gap-2">
{app.prompts.map((prompt, i) => (
<button
key={i}
className="prompt-pill text-xs cursor-pointer hover:scale-105 transition-transform"
onClick={() => sendMessage(prompt)}
>
&ldquo;{prompt}&rdquo;
</button>
))}
</div>
</div>
))}
</section>
{/* Info Banner */}
<section className="glass-card">
<div className="flex items-start gap-4">
<div className="flex-shrink-0 w-10 h-10 rounded-lg bg-gradient-to-br from-[var(--color-lilac)] to-[var(--color-mint)] flex items-center justify-center text-white">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="12" cy="12" r="10" />
<path d="M12 16v-4" />
<path d="M12 8h.01" />
</svg>
</div>
<div>
<h4 className="font-semibold text-[var(--color-text-primary)] mb-1">
How It Works
</h4>
<p className="text-sm text-[var(--color-text-secondary)]">
Each app renders in a sandboxed iframe within the chat. The UI
communicates with the MCP server via JSON-RPC over postMessage,
enabling real-time interactions like selecting flight seats,
booking hotel rooms, or executing trades.
</p>
<p className="mt-3 text-xs text-[var(--color-text-tertiary)]">
MCP Server:{" "}
<code className="glass-subtle px-2 py-0.5 rounded text-[var(--color-text-secondary)]">
cd mcp-server && npm run dev
</code>
</p>
</div>
</div>
</section>
</main>
{/* CopilotKit Chat UI - Sidebar on desktop, Popup on mobile */}
{isDesktop ? (
<CopilotSidebar defaultOpen={true} width="50%" />
) : (
<CopilotPopup
defaultOpen={false}
labels={{
modalHeaderTitle: "MCP Apps Assistant",
chatInputPlaceholder: "What would you like to try today?",
}}
/>
)}
</div>
);
}
@@ -0,0 +1,22 @@
import { useEffect, useState } from "react";
export function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState<boolean>(false);
useEffect(() => {
if (
typeof window === "undefined" ||
typeof window.matchMedia === "undefined"
)
return;
const mediaQueryList = window.matchMedia(query);
const updateMatch = () => setMatches(mediaQueryList.matches);
updateMatch();
mediaQueryList.addEventListener("change", updateMatch);
return () => mediaQueryList.removeEventListener("change", updateMatch);
}, [query]);
return matches;
}
export default useMediaQuery;