diff --git a/examples/map-server/README.md b/examples/map-server/README.md
new file mode 100644
index 00000000..c504eeab
--- /dev/null
+++ b/examples/map-server/README.md
@@ -0,0 +1,90 @@
+# Example: Interactive Map
+
+Interactive 3D globe viewer using CesiumJS with OpenStreetMap tiles. Demonstrates geocoding integration and full MCP App capabilities.
+
+## Features
+
+- **3D Globe Rendering**: Interactive CesiumJS globe with rotation, zoom, and 3D perspective
+- **Geocoding**: Search for places using OpenStreetMap Nominatim (no API key required)
+- **OpenStreetMap Tiles**: Uses free OSM tile server (no Cesium Ion token needed)
+- **Dynamic Loading**: CesiumJS loaded from CDN at runtime for smaller bundle size
+
+## Running
+
+1. Install dependencies:
+
+ ```bash
+ npm install
+ ```
+
+2. Build and start the server:
+
+ ```bash
+ npm run start:http # for Streamable HTTP transport
+ # OR
+ npm run start:stdio # for stdio transport
+ ```
+
+3. View using the [`basic-host`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host) example or another MCP Apps-compatible host.
+
+## Tools
+
+### `geocode`
+
+Search for places by name or address. Returns coordinates and bounding boxes.
+
+```json
+{
+ "query": "Eiffel Tower"
+}
+```
+
+Returns up to 5 matches with lat/lon coordinates and bounding boxes.
+
+### `show-map`
+
+Display the 3D globe zoomed to a bounding box.
+
+```json
+{
+ "west": 2.29,
+ "south": 48.85,
+ "east": 2.3,
+ "north": 48.86,
+ "label": "Eiffel Tower"
+}
+```
+
+Defaults to London if no coordinates provided.
+
+## Architecture
+
+### Server (`server.ts`)
+
+Exposes two tools:
+
+- `geocode` - Queries OpenStreetMap Nominatim API with rate limiting
+- `show-map` - Renders the CesiumJS globe UI at a specified location
+
+Configures Content Security Policy to allow fetching tiles from OSM and Cesium CDN.
+
+### App (`src/mcp-app.ts`)
+
+Vanilla TypeScript app that:
+
+- Dynamically loads CesiumJS from CDN
+- Initializes globe with OpenStreetMap imagery (no Ion token)
+- Receives tool inputs via the MCP App SDK
+- Handles camera navigation to specified bounding boxes
+
+## Key Files
+
+- [`server.ts`](server.ts) - MCP server with geocode and show-map tools
+- [`mcp-app.html`](mcp-app.html) / [`src/mcp-app.ts`](src/mcp-app.ts) - CesiumJS globe UI
+- [`server-utils.ts`](server-utils.ts) - HTTP server utilities
+
+## Notes
+
+- Rate limiting is applied to Nominatim requests (1 request per second per their usage policy)
+- The globe works in sandboxed iframes with appropriate CSP configuration
+- No external API keys required - uses only open data sources
diff --git a/examples/map-server/mcp-app.html b/examples/map-server/mcp-app.html
new file mode 100644
index 00000000..e46c7ebb
--- /dev/null
+++ b/examples/map-server/mcp-app.html
@@ -0,0 +1,73 @@
+
+
+
+
+
+ CesiumJS Globe
+
+
+
+
+
+
+
Loading globe...
+
+
+
diff --git a/examples/map-server/package.json b/examples/map-server/package.json
new file mode 100644
index 00000000..2385f503
--- /dev/null
+++ b/examples/map-server/package.json
@@ -0,0 +1,46 @@
+{
+ "name": "@modelcontextprotocol/server-map",
+ "version": "0.1.0",
+ "type": "module",
+ "description": "MCP App Server example with CesiumJS 3D globe and geocoding",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/modelcontextprotocol/ext-apps",
+ "directory": "examples/map-server"
+ },
+ "license": "MIT",
+ "main": "server.ts",
+ "files": [
+ "server.ts",
+ "server-utils.ts",
+ "dist"
+ ],
+ "scripts": {
+ "build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build",
+ "watch": "cross-env INPUT=mcp-app.html vite build --watch",
+ "serve:http": "bun server.ts",
+ "serve:stdio": "bun server.ts --stdio",
+ "start": "npm run start:http",
+ "start:http": "cross-env NODE_ENV=development npm run build && npm run serve:http",
+ "start:stdio": "cross-env NODE_ENV=development npm run build && npm run serve:stdio",
+ "dev": "cross-env NODE_ENV=development concurrently 'npm run watch' 'npm run serve:http'",
+ "prepublishOnly": "npm run build"
+ },
+ "dependencies": {
+ "@modelcontextprotocol/ext-apps": "^0.3.1",
+ "@modelcontextprotocol/sdk": "^1.24.0",
+ "zod": "^4.1.13"
+ },
+ "devDependencies": {
+ "@types/cors": "^2.8.19",
+ "@types/express": "^5.0.0",
+ "@types/node": "^22.0.0",
+ "concurrently": "^9.2.1",
+ "cors": "^2.8.5",
+ "cross-env": "^10.1.0",
+ "express": "^5.1.0",
+ "typescript": "^5.9.3",
+ "vite": "^6.0.0",
+ "vite-plugin-singlefile": "^2.3.0"
+ }
+}
diff --git a/examples/map-server/server-utils.ts b/examples/map-server/server-utils.ts
new file mode 100644
index 00000000..c700c818
--- /dev/null
+++ b/examples/map-server/server-utils.ts
@@ -0,0 +1,68 @@
+/**
+ * Shared utilities for running MCP servers with Streamable HTTP transport.
+ */
+
+import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
+import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
+import cors from "cors";
+import type { Request, Response } from "express";
+
+export interface ServerOptions {
+ port: number;
+ name?: string;
+}
+
+/**
+ * Starts an MCP server with Streamable HTTP transport in stateless mode.
+ *
+ * @param createServer - Factory function that creates a new McpServer instance per request.
+ * @param options - Server configuration options.
+ */
+export async function startServer(
+ createServer: () => McpServer,
+ options: ServerOptions,
+): Promise {
+ const { port, name = "MCP Server" } = options;
+
+ const app = createMcpExpressApp({ host: "0.0.0.0" });
+ app.use(cors());
+
+ app.all("/mcp", async (req: Request, res: Response) => {
+ const server = createServer();
+ const transport = new StreamableHTTPServerTransport({
+ sessionIdGenerator: undefined,
+ });
+
+ res.on("close", () => {
+ transport.close().catch(() => {});
+ server.close().catch(() => {});
+ });
+
+ try {
+ await server.connect(transport);
+ await transport.handleRequest(req, res, req.body);
+ } catch (error) {
+ console.error("MCP error:", error);
+ if (!res.headersSent) {
+ res.status(500).json({
+ jsonrpc: "2.0",
+ error: { code: -32603, message: "Internal server error" },
+ id: null,
+ });
+ }
+ }
+ });
+
+ const httpServer = app.listen(port, () => {
+ console.log(`${name} listening on http://localhost:${port}/mcp`);
+ });
+
+ const shutdown = () => {
+ console.log("\nShutting down...");
+ httpServer.close(() => process.exit(0));
+ };
+
+ process.on("SIGINT", shutdown);
+ process.on("SIGTERM", shutdown);
+}
diff --git a/examples/map-server/server.ts b/examples/map-server/server.ts
new file mode 100644
index 00000000..b36db931
--- /dev/null
+++ b/examples/map-server/server.ts
@@ -0,0 +1,268 @@
+/**
+ * CesiumJS Map MCP Server
+ *
+ * Provides tools for:
+ * - geocode: Search for places using OpenStreetMap Nominatim
+ * - show-map: Display an interactive 3D globe at a given location
+ */
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
+import type {
+ CallToolResult,
+ ReadResourceResult,
+} from "@modelcontextprotocol/sdk/types.js";
+import fs from "node:fs/promises";
+import path from "node:path";
+import { z } from "zod";
+import {
+ registerAppTool,
+ registerAppResource,
+ RESOURCE_MIME_TYPE,
+ RESOURCE_URI_META_KEY,
+} from "@modelcontextprotocol/ext-apps/server";
+import { startServer } from "./server-utils.js";
+
+const DIST_DIR = path.join(import.meta.dirname, "dist");
+const RESOURCE_URI = "ui://cesium-map/mcp-app.html";
+
+// Nominatim API response type
+interface NominatimResult {
+ place_id: number;
+ licence: string;
+ osm_type: string;
+ osm_id: number;
+ lat: string;
+ lon: string;
+ display_name: string;
+ boundingbox: [string, string, string, string]; // [south, north, west, east]
+ class: string;
+ type: string;
+ importance: number;
+}
+
+// Rate limiting for Nominatim (1 request per second per their usage policy)
+let lastNominatimRequest = 0;
+const NOMINATIM_RATE_LIMIT_MS = 1100; // 1.1 seconds to be safe
+
+/**
+ * Query Nominatim geocoding API with rate limiting
+ */
+async function geocodeWithNominatim(query: string): Promise {
+ // Respect rate limit
+ const now = Date.now();
+ const timeSinceLastRequest = now - lastNominatimRequest;
+ if (timeSinceLastRequest < NOMINATIM_RATE_LIMIT_MS) {
+ await new Promise((resolve) =>
+ setTimeout(resolve, NOMINATIM_RATE_LIMIT_MS - timeSinceLastRequest),
+ );
+ }
+ lastNominatimRequest = Date.now();
+
+ const params = new URLSearchParams({
+ q: query,
+ format: "json",
+ limit: "5",
+ });
+
+ const response = await fetch(
+ `https://nominatim.openstreetmap.org/search?${params}`,
+ {
+ headers: {
+ "User-Agent":
+ "MCP-CesiumMap-Example/1.0 (https://github.com/modelcontextprotocol)",
+ },
+ },
+ );
+
+ if (!response.ok) {
+ throw new Error(
+ `Nominatim API error: ${response.status} ${response.statusText}`,
+ );
+ }
+
+ return response.json();
+}
+
+/**
+ * Creates a new MCP server instance with tools and resources registered.
+ * Each HTTP session needs its own server instance because McpServer only supports one transport.
+ */
+export function createServer(): McpServer {
+ const server = new McpServer({
+ name: "CesiumJS Map Server",
+ version: "1.0.0",
+ });
+
+ // CSP configuration for external tile sources
+ const cspMeta = {
+ ui: {
+ csp: {
+ // Allow fetching tiles from OSM (tiles + geocoding) and Cesium assets
+ connectDomains: [
+ "https://*.openstreetmap.org", // OSM tiles + Nominatim geocoding
+ "https://cesium.com",
+ "https://*.cesium.com",
+ ],
+ // Allow loading tile images, scripts, and Cesium CDN resources
+ resourceDomains: [
+ "https://*.openstreetmap.org", // OSM map tiles (covers tile.openstreetmap.org)
+ "https://cesium.com",
+ "https://*.cesium.com",
+ ],
+ },
+ },
+ };
+
+ // Register the CesiumJS map resource with CSP for external tile sources
+ registerAppResource(
+ server,
+ RESOURCE_URI,
+ RESOURCE_URI,
+ { mimeType: RESOURCE_MIME_TYPE },
+ async (): Promise => {
+ const html = await fs.readFile(
+ path.join(DIST_DIR, "mcp-app.html"),
+ "utf-8",
+ );
+ return {
+ contents: [
+ // _meta must be on the content item, not the resource metadata
+ {
+ uri: RESOURCE_URI,
+ mimeType: RESOURCE_MIME_TYPE,
+ text: html,
+ _meta: cspMeta,
+ },
+ ],
+ };
+ },
+ );
+
+ // show-map tool - displays the CesiumJS globe
+ // Default bounding box: London area
+ registerAppTool(
+ server,
+ "show-map",
+ {
+ title: "Show Map",
+ description:
+ "Display an interactive world map zoomed to a specific bounding box. Use the GeoCode tool to find the bounding box of a location.",
+ inputSchema: {
+ west: z
+ .number()
+ .optional()
+ .default(-0.5)
+ .describe("Western longitude (-180 to 180)"),
+ south: z
+ .number()
+ .optional()
+ .default(51.3)
+ .describe("Southern latitude (-90 to 90)"),
+ east: z
+ .number()
+ .optional()
+ .default(0.3)
+ .describe("Eastern longitude (-180 to 180)"),
+ north: z
+ .number()
+ .optional()
+ .default(51.7)
+ .describe("Northern latitude (-90 to 90)"),
+ label: z
+ .string()
+ .optional()
+ .describe("Optional label to display on the map"),
+ },
+ _meta: { [RESOURCE_URI_META_KEY]: RESOURCE_URI },
+ },
+ async ({ west, south, east, north, label }): Promise => ({
+ content: [
+ {
+ type: "text",
+ text: `Displaying globe at: W:${west.toFixed(4)}, S:${south.toFixed(4)}, E:${east.toFixed(4)}, N:${north.toFixed(4)}${label ? ` (${label})` : ""}`,
+ },
+ ],
+ }),
+ );
+
+ // geocode tool - searches for places using Nominatim (no UI)
+ server.registerTool(
+ "geocode",
+ {
+ title: "Geocode",
+ description:
+ "Search for places using OpenStreetMap. Returns coordinates and bounding boxes for up to 5 matches.",
+ inputSchema: {
+ query: z
+ .string()
+ .describe(
+ "Place name or address to search for (e.g., 'Paris', 'Golden Gate Bridge', '1600 Pennsylvania Ave')",
+ ),
+ },
+ },
+ async ({ query }): Promise => {
+ try {
+ const results = await geocodeWithNominatim(query);
+
+ if (results.length === 0) {
+ return {
+ content: [
+ { type: "text", text: `No results found for "${query}"` },
+ ],
+ };
+ }
+
+ const formattedResults = results.map((r) => ({
+ displayName: r.display_name,
+ lat: parseFloat(r.lat),
+ lon: parseFloat(r.lon),
+ boundingBox: {
+ south: parseFloat(r.boundingbox[0]),
+ north: parseFloat(r.boundingbox[1]),
+ west: parseFloat(r.boundingbox[2]),
+ east: parseFloat(r.boundingbox[3]),
+ },
+ type: r.type,
+ importance: r.importance,
+ }));
+
+ const textContent = formattedResults
+ .map(
+ (r, i) =>
+ `${i + 1}. ${r.displayName}\n Coordinates: ${r.lat.toFixed(6)}, ${r.lon.toFixed(6)}\n Bounding box: W:${r.boundingBox.west.toFixed(4)}, S:${r.boundingBox.south.toFixed(4)}, E:${r.boundingBox.east.toFixed(4)}, N:${r.boundingBox.north.toFixed(4)}`,
+ )
+ .join("\n\n");
+
+ return {
+ content: [{ type: "text", text: textContent }],
+ };
+ } catch (error) {
+ return {
+ content: [
+ {
+ type: "text",
+ text: `Geocoding error: ${error instanceof Error ? error.message : String(error)}`,
+ },
+ ],
+ isError: true,
+ };
+ }
+ },
+ );
+
+ return server;
+}
+
+async function main() {
+ if (process.argv.includes("--stdio")) {
+ await createServer().connect(new StdioServerTransport());
+ } else {
+ const port = parseInt(process.env.PORT ?? "3001", 10);
+ await startServer(createServer, { port, name: "CesiumJS Map Server" });
+ }
+}
+
+main().catch((e) => {
+ console.error(e);
+ process.exit(1);
+});
diff --git a/examples/map-server/src/mcp-app.ts b/examples/map-server/src/mcp-app.ts
new file mode 100644
index 00000000..b78aee79
--- /dev/null
+++ b/examples/map-server/src/mcp-app.ts
@@ -0,0 +1,843 @@
+/**
+ * CesiumJS Globe MCP App
+ *
+ * Displays a 3D globe using CesiumJS with OpenStreetMap tiles.
+ * Receives initial bounding box from the show-map tool and exposes
+ * a navigate-to tool for the host to control navigation.
+ */
+import { App } from "@modelcontextprotocol/ext-apps";
+import { z } from "zod";
+
+// TypeScript declaration for Cesium loaded from CDN
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+declare let Cesium: any;
+
+const CESIUM_VERSION = "1.123";
+const CESIUM_BASE_URL = `https://cesium.com/downloads/cesiumjs/releases/${CESIUM_VERSION}/Build/Cesium`;
+
+/**
+ * Dynamically load CesiumJS from CDN
+ * This is necessary because external
+
+