Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
23b9e0c
feat: Add CesiumJS map server example
ochafik Jan 11, 2026
bdb85d0
comment out app.registerTool call for now
ochafik Jan 11, 2026
83ecc37
prettier
ochafik Jan 11, 2026
e841af3
fix error handling in server-utils.ts
ochafik Jan 11, 2026
661f388
refactor: rename cesium-map-server to map-server
ochafik Jan 11, 2026
c1e3cd6
style: add min-height of 400px to map container
ochafik Jan 11, 2026
3ffa9b6
feat(map-server): add fullscreen support, clean UI, sharp rendering
ochafik Jan 11, 2026
c9c3eef
feat(map-server): add reverse geocoding (logging only) and rounded co…
ochafik Jan 11, 2026
a060346
fix(map-server): fix pixelated rendering on high-DPI displays
ochafik Jan 11, 2026
b868728
tweaks
ochafik Jan 11, 2026
1b15ba7
update model context w/ current location
ochafik Jan 11, 2026
36c6484
fix(map-server): improve rendering quality and add visible extent log…
ochafik Jan 11, 2026
a5e7302
fix(map-server): direct positioning + wait for tiles before showing
ochafik Jan 11, 2026
cb6f9ab
feat(map-server): use viewbox search to find visible places
ochafik Jan 11, 2026
6aeefab
fix(map-server): use CARTO @2x retina tiles for sharp rendering
ochafik Jan 11, 2026
da8f076
fix(map-server): use OSM tiles and fix reverse geocoding
ochafik Jan 11, 2026
74086b1
fix(map-server): use *.openstreetmap.org wildcard in CSP
ochafik Jan 11, 2026
653981d
feat(map-server): multi-point sampling for visible places
ochafik Jan 11, 2026
99fce46
format
ochafik Jan 11, 2026
d9a16d8
fix(map-server): fix Express listen callback + add e2e tests
ochafik Jan 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions examples/map-server/README.md
Original file line number Diff line number Diff line change
@@ -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
73 changes: 73 additions & 0 deletions examples/map-server/mcp-app.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CesiumJS Globe</title>
<!-- CesiumJS is loaded dynamically from CDN in mcp-app.ts because static
<script src=""> tags don't work in srcdoc iframes -->
<style>
html, body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
#cesiumContainer {
width: 100%;
height: 100%;
border-radius: .75rem;
overflow: hidden;
}
#fullscreen-btn {
position: absolute;
top: 10px;
right: 10px;
width: 36px;
height: 36px;
background: rgba(0, 0, 0, 0.7);
border: none;
border-radius: 6px;
cursor: pointer;
z-index: 1000;
display: none;
align-items: center;
justify-content: center;
transition: background 0.2s;
}
#fullscreen-btn:hover {
background: rgba(0, 0, 0, 0.85);
}
#fullscreen-btn svg {
width: 20px;
height: 20px;
fill: white;
}
#loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 20px 30px;
border-radius: 8px;
font-size: 16px;
z-index: 1001;
}
</style>
</head>
<body>
<div id="cesiumContainer"></div>
<button id="fullscreen-btn" title="Toggle fullscreen">
<!-- Expand icon (shown when inline) -->
<svg id="expand-icon" viewBox="0 0 24 24"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/></svg>
<!-- Compress icon (shown when fullscreen) -->
<svg id="compress-icon" style="display:none" viewBox="0 0 24 24"><path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/></svg>
</button>
<div id="loading">Loading globe...</div>
<script type="module" src="/src/mcp-app.ts"></script>
</body>
</html>
46 changes: 46 additions & 0 deletions examples/map-server/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
68 changes: 68 additions & 0 deletions examples/map-server/server-utils.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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);
}
Loading
Loading