Waiting for tool result...
@@ -149,22 +125,33 @@ export function AppComponent() {
);
}
+ // Dashboard view
+ if (renderView === "dashboard") {
+ return
- {widgetType === "tool-call" && (
+
+ {/* Show navbar when navigating from dashboard */}
+ {currentView && (
+ setCurrentView(null)}
+ />
+ )}
+
+ {renderView === "tool-call" && (
)}
- {widgetType === "open-link" && (
+ {renderView === "open-link" && (
)}
- {widgetType === "read-resource" && (
-
- )}
- {widgetType === "message" && }
- {widgetType === "csp-test" && (
+
+ {renderView === "message" && }
+ {renderView === "csp-test" && (
)}
- {widgetType === "size-change" && (
+ {renderView === "size-change" && (
)}
diff --git a/widget/components/Navbar.tsx b/widget/components/Navbar.tsx
new file mode 100644
index 0000000..43cf170
--- /dev/null
+++ b/widget/components/Navbar.tsx
@@ -0,0 +1,34 @@
+/**
+ * Shared Navigation Bar
+ * Shows back button when viewing individual widgets
+ */
+
+import { ArrowLeft } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import type { App } from "@modelcontextprotocol/ext-apps/react";
+
+interface NavbarProps {
+ app: App;
+ onBack: () => void;
+}
+
+export function Navbar({ app, onBack }: NavbarProps) {
+ const handleBack = () => {
+ // Reset size to 400px when going back to dashboard
+ app.sendSizeChange({ height: 400 });
+ onBack();
+ };
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/widget/widgets/Dashboard.tsx b/widget/widgets/Dashboard.tsx
new file mode 100644
index 0000000..ea59a36
--- /dev/null
+++ b/widget/widgets/Dashboard.tsx
@@ -0,0 +1,88 @@
+/**
+ * Dashboard Widget - Shows all available widgets for navigation
+ */
+
+import {
+ MousePointerClick,
+ ExternalLink,
+ MessageSquare,
+ Shield,
+ Maximize2
+} from "lucide-react";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import type { WidgetType } from "../App";
+
+interface DashboardProps {
+ onNavigate: (widget: WidgetType) => void;
+}
+
+const WIDGETS = [
+ {
+ id: "tool-call" as WidgetType,
+ name: "Tool Call",
+ description: "Demonstrates tools/call API",
+ icon: MousePointerClick
+ },
+ {
+ id: "open-link" as WidgetType,
+ name: "Open Link",
+ description: "Demonstrates the ui/open-link API",
+ icon: ExternalLink
+ },
+ {
+ id: "message" as WidgetType,
+ name: "Message",
+ description: "Demonstrates the ui/message API",
+ icon: MessageSquare
+ },
+ {
+ id: "csp-test" as WidgetType,
+ name: "CSP Test",
+ description: "Tests Content Security Policy",
+ icon: Shield
+ },
+ {
+ id: "size-change" as WidgetType,
+ name: "Size Change",
+ description: "Demonstrates the ui/notifications/size-change API",
+ icon: Maximize2
+ },
+];
+
+export function Dashboard({ onNavigate }: DashboardProps) {
+ return (
+
+
+
MCP Apps Dashboard
+
+ Select a widget to explore different MCP APIs and capabilities
+
+
+
+
+ {WIDGETS.map((widget) => {
+ const Icon = widget.icon;
+ return (
+
onNavigate(widget.id)}
+ >
+
+
+
+
+ {widget.description}
+
+
+ );
+ })}
+
+
+ );
+}
diff --git a/widget/widgets/OpenLinkWidget.tsx b/widget/widgets/OpenLinkWidget.tsx
index e9a4b6e..9e084cb 100644
--- a/widget/widgets/OpenLinkWidget.tsx
+++ b/widget/widgets/OpenLinkWidget.tsx
@@ -2,103 +2,43 @@
* Open Link Widget - Demonstrates ui/open-link API
*/
-import { useState, useEffect } from "react";
-import { Button } from "@/components/ui/button";
-import { ExternalLink, Droplets, Wind, MapPin, Loader2 } from "lucide-react";
+import { useState } from "react";
import type { App } from "@modelcontextprotocol/ext-apps/react";
-interface WeatherData {
- temperature: number;
- humidity: number;
- windSpeed: number;
- condition: string;
-}
-
interface OpenLinkWidgetProps {
app: App;
toolInput: { arguments: Record
} | null;
toolResult: { structuredContent?: Record } | null;
}
-// Map Open-Meteo weather codes to human-readable conditions
-function getWeatherCondition(code: number): string {
- if (code === 0) return "Clear sky";
- if (code <= 3) return "Partly cloudy";
- if (code <= 49) return "Foggy";
- if (code <= 59) return "Drizzle";
- if (code <= 69) return "Rain";
- if (code <= 79) return "Snow";
- if (code <= 99) return "Thunderstorm";
- return "Unknown";
-}
+const LINKS = [
+ {
+ url: "https://mcpjam.com",
+ label: "MCPJam",
+ description: "The Official MCPJam Inspector Website"
+ },
+ {
+ url: "https://mcpui.dev",
+ label: "MCP UI",
+ description: "MCP Apps documentation and resources"
+ },
+ {
+ url: "https://modelcontextprotocol.io",
+ label: "Model Context Protocol",
+ description: "Official MCP documentation and guides"
+ },
+ {
+ url: "https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx",
+ label: "SEP-1865 Specification",
+ description: "Technical specification for MCP Apps"
+ }
+];
-export function OpenLinkWidget({ app, toolInput, toolResult }: OpenLinkWidgetProps) {
- const [location, setLocation] = useState(null);
- const [weather, setWeather] = useState(null);
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState(null);
+export function OpenLinkWidget({ app }: OpenLinkWidgetProps) {
const [linkOpened, setLinkOpened] = useState(null);
- useEffect(() => {
- if (toolResult?.structuredContent) {
- const data = toolResult.structuredContent as { location?: string };
- if (data.location) setLocation(data.location);
- } else if (toolInput?.arguments) {
- const args = toolInput.arguments as { location?: string };
- if (args.location) setLocation(args.location);
- }
- }, [toolResult, toolInput]);
-
- useEffect(() => {
- if (!location) return;
-
- const fetchWeather = async () => {
- setLoading(true);
- setError(null);
-
- try {
- // First, geocode the location to get coordinates
- const geoResponse = await fetch(
- `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(location)}&count=1`
- );
- const geoData = await geoResponse.json();
-
- if (!geoData.results || geoData.results.length === 0) {
- setError("Location not found");
- setLoading(false);
- return;
- }
-
- const { latitude, longitude } = geoData.results[0];
-
- // Then fetch weather data
- const weatherResponse = await fetch(
- `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code`
- );
- const weatherData = await weatherResponse.json();
-
- if (weatherData.current) {
- setWeather({
- temperature: Math.round(weatherData.current.temperature_2m),
- humidity: weatherData.current.relative_humidity_2m,
- windSpeed: Math.round(weatherData.current.wind_speed_10m),
- condition: getWeatherCondition(weatherData.current.weather_code),
- });
- }
- } catch (err) {
- console.error("Failed to fetch weather:", err);
- setError("Failed to fetch weather data");
- } finally {
- setLoading(false);
- }
- };
-
- fetchWeather();
- }, [location]);
-
const handleOpenLink = async (url: string, label: string) => {
try {
- // Open link directly via the SDK
await app.sendOpenLink({ url });
setLinkOpened(label);
setTimeout(() => setLinkOpened(null), 2000);
@@ -107,113 +47,28 @@ export function OpenLinkWidget({ app, toolInput, toolResult }: OpenLinkWidgetPro
}
};
- if (!location) {
- return (
-
- );
- }
-
- if (loading) {
- return (
-
-
-
- );
- }
-
- if (error) {
- return (
-
-
{error}
-
-
-
-
- );
- }
-
return (
-
- {/* Location */}
-
-
-
{location}
+
+
+
MCP Resources
+
+ Explore these MCP-related resources and documentation
+
- {/* Temperature */}
- {weather && (
- <>
-
-
-
- {weather.temperature}
-
-
°C
-
-
- {weather.condition}
-
-
-
- {/* Stats */}
-
-
-
- {weather.humidity}%
-
-
-
-
{weather.windSpeed} km/h
+
+ {LINKS.map((link) => (
+
handleOpenLink(link.url, link.label)}
+ >
+
+
{link.label}
+
{link.description}
- >
- )}
-
- {/* Quick Links */}
-
-
-
+ ))}
{/* Success Toast */}
diff --git a/widget/widgets/ReadResourceWidget.tsx b/widget/widgets/ReadResourceWidget.tsx
deleted file mode 100644
index d731a93..0000000
--- a/widget/widgets/ReadResourceWidget.tsx
+++ /dev/null
@@ -1,116 +0,0 @@
-/**
- * Read Resource Widget - Demonstrates resources/read API
- */
-
-import { useState, useEffect } from "react";
-import { Button } from "@/components/ui/button";
-import { BookOpen } from "lucide-react";
-import type { App } from "@modelcontextprotocol/ext-apps/react";
-
-interface Resource {
- id: string;
- title: string;
- content: string;
-}
-
-interface AvailableResource {
- id: string;
- title: string;
-}
-
-interface ReadResourceWidgetProps {
- app: App;
- toolInput: { arguments: Record
} | null;
- toolResult: { structuredContent?: Record } | null;
-}
-
-export function ReadResourceWidget({ app, toolResult }: ReadResourceWidgetProps) {
- const [availableResources, setAvailableResources] = useState([]);
- const [currentResource, setCurrentResource] = useState(null);
- const [loading, setLoading] = useState(false);
- const [selectedId, setSelectedId] = useState(null);
-
- useEffect(() => {
- if (toolResult?.structuredContent?.availableTips) {
- setAvailableResources(toolResult.structuredContent.availableTips as AvailableResource[]);
- }
- }, [toolResult]);
-
- const fetchResource = async (resourceId: string) => {
- setLoading(true);
- setSelectedId(resourceId);
-
- try {
- // Read the resource directly via the SDK
- const result = await app.readServerResource({ uri: `tips://${resourceId}` });
- const textContent = result?.contents?.[0]?.text;
-
- if (textContent) {
- const resource = JSON.parse(textContent);
- setCurrentResource(resource);
- }
- } catch (err) {
- console.error("Failed to read resource:", err);
- } finally {
- setLoading(false);
- }
- };
-
- if (availableResources.length === 0) {
- return (
-
- );
- }
-
- return (
-
- {/* Content Display */}
-
- {loading && (
-
- Reading...
-
- )}
-
- {!loading && currentResource && (
- <>
-
- {currentResource.title}
-
-
- {currentResource.content}
-
-
- tips://{currentResource.id}
-
- >
- )}
-
- {!loading && !currentResource && (
-
- Select a topic to read
-
- )}
-
-
- {/* Topic Buttons */}
-
- {availableResources.map((resource) => (
-
- ))}
-
-
- );
-}
diff --git a/widget/widgets/SizeChangeWidget.tsx b/widget/widgets/SizeChangeWidget.tsx
index 8259a28..2f94ebd 100644
--- a/widget/widgets/SizeChangeWidget.tsx
+++ b/widget/widgets/SizeChangeWidget.tsx
@@ -1,10 +1,9 @@
/**
- * Size Change Widget - Demonstrates ui/size-change API
+ * Size Change Widget - Demonstrates ui/notifications/size-change API
*/
-import { useState, useEffect } from "react";
+import { useState, useEffect, useRef } from "react";
import { Button } from "@/components/ui/button";
-import { Loader2 } from "lucide-react";
import type { App } from "@modelcontextprotocol/ext-apps/react";
interface SizeChangeWidgetProps {
@@ -19,57 +18,81 @@ const MAX_HEIGHT = 800;
export function SizeChangeWidget({ app, toolInput, toolResult }: SizeChangeWidgetProps) {
const [height, setHeight] = useState(300);
- const [loading, setLoading] = useState(false);
const [lastChange, setLastChange] = useState(null);
-
- // Send initial size on mount
- useEffect(() => {
- app.sendSizeChange({ height: 300 });
- }, [app]);
+ const [autoResize, setAutoResize] = useState(false);
+ const contentRef = useRef(null);
useEffect(() => {
if (toolInput?.arguments?.height !== undefined) {
const inputHeight = toolInput.arguments.height as number;
const clampedHeight = Math.max(MIN_HEIGHT, Math.min(MAX_HEIGHT, inputHeight));
setHeight(clampedHeight);
- app.sendSizeChange({ height: clampedHeight });
}
- }, [toolInput, app]);
+ }, [toolInput]);
useEffect(() => {
if (toolResult?.structuredContent?.height !== undefined) {
const resultHeight = toolResult.structuredContent.height as number;
const clampedHeight = Math.max(MIN_HEIGHT, Math.min(MAX_HEIGHT, resultHeight));
setHeight(clampedHeight);
- app.sendSizeChange({ height: clampedHeight });
}
- }, [toolResult, app]);
+ }, [toolResult]);
+
+ // Auto resize effect
+ useEffect(() => {
+ if (!autoResize || !contentRef.current) return;
+
+ const updateHeight = () => {
+ if (contentRef.current) {
+ const contentHeight = contentRef.current.scrollHeight;
+ const newHeight = Math.max(MIN_HEIGHT, Math.min(MAX_HEIGHT, contentHeight));
+ setHeight(newHeight);
+ }
+ };
+
+ // Initial height calculation
+ updateHeight();
+
+ // Set up ResizeObserver to watch for content changes
+ const resizeObserver = new ResizeObserver(() => {
+ updateHeight();
+ });
+
+ resizeObserver.observe(contentRef.current);
- const handleHeightChange = async (newHeight: number) => {
+ return () => {
+ resizeObserver.disconnect();
+ };
+ }, [autoResize]);
+
+ const handleHeightChange = (newHeight: number) => {
// Clamp height to valid range
const clampedHeight = Math.max(MIN_HEIGHT, Math.min(MAX_HEIGHT, newHeight));
if (clampedHeight === height) return;
- setLoading(true);
- try {
- // Send size change notification to host
- app.sendSizeChange({ height: clampedHeight });
- const change = clampedHeight - height;
- setHeight(clampedHeight);
- setLastChange(change);
- setTimeout(() => setLastChange(null), 1000);
- } catch (err) {
- console.error("Failed to change size:", err);
- } finally {
- setLoading(false);
- }
+ const change = clampedHeight - height;
+ setHeight(clampedHeight);
+ setLastChange(change);
+ setTimeout(() => setLastChange(null), 1000);
};
return (
-
+
+ {/* Auto Resize Toggle */}
+
+
Auto Resize
+
+
+
{/* Main content - centered */}
-
+
{/* Height Display */}
@@ -85,7 +108,7 @@ export function SizeChangeWidget({ app, toolInput, toolResult }: SizeChangeWidge
)}
- {/* Preset Buttons */}
+ {/* Preset Buttons - disabled when auto resize is on */}
{PRESET_HEIGHTS.map((presetHeight) => (
))}
-
- {/* Loading indicator */}
- {loading && (
-
-
- Updating size...
-
- )}