-
Notifications
You must be signed in to change notification settings - Fork 61
examples: add virtual-desktop-server with Docker-based VNC viewer #241
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Add a new example MCP server that manages virtual desktops using Docker containers with VNC access: - ListDesktops/CreateDesktop/ViewDesktop/ShutdownDesktop tools - Embedded noVNC viewer as MCP App with WebSocket connection - Light/dark theme support with CSS variables - Fullscreen toggle, disconnect/shutdown buttons, open home folder - Fixed reconnect issues (resizeSession=false, separate connection state) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add basic tests (server listing, list-desktops tool) - Add Docker-dependent tests (VNC viewer, screenshot, disconnect/reconnect) - Docker tests require ENABLE_DOCKER_TESTS=1 env var - Add test:e2e:docker:dind scripts for Docker-in-Docker testing - Fix useApp cleanup to properly close app on unmount 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
| if (app) { | ||
| app.openLink({ url: extractedInfo.url }); | ||
| } else { | ||
| window.open(extractedInfo.url, "_blank"); |
Check warning
Code scanning / CodeQL
Client-side URL redirect Medium
user-provided value
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 7 hours ago
In general, to fix client-side URL redirect issues you should not pass user-controlled URLs directly into navigation sinks (window.location, window.open, etc.). Instead, either (1) enforce an allow-list of allowed destinations, or (2) at minimum normalize and validate the URL scheme and (optionally) hostname/path before using it, rejecting or ignoring unsafe values.
For this snippet, the best fix with minimal behavior change is to introduce a small client-side validator for DesktopInfo.url and use it both when constructing the standalone DesktopInfo (so bad URLs are never stored) and immediately before calling window.open. The validator should only accept HTTP(S) URLs and can be conservative about what hosts/paths are permitted if desired. Because we must not assume anything about the rest of the project, we will implement a simple isSafeHttpUrl helper in this file using the standard URL class and basic checks:
- Require the URL to parse successfully.
- Require
protocolto behttp:orhttps:.
(If you wanted stricter checks, you could further restrict hostnames, but that would risk changing behavior for legitimate test setups.)
Concretely:
- Add an
isSafeHttpUrlfunction near the other helpers (e.g., abovegetStandaloneDesktopInfo). - In
getStandaloneDesktopInfo, after derivingurlfromwsUrl, validate it withisSafeHttpUrl. If invalid, returnnullso standalone mode is not used. - In
handleOpenInBrowser, before callingwindow.open, checkisSafeHttpUrl(extractedInfo.url). If it fails, simply do nothing (or you could integrate with existing error-handling by settingsetErrorMessage, but that would be a behavior change; we’ll keep it minimal and silent).
No new external dependencies are needed; we rely on the built-in URL API.
-
Copy modified lines R132-R144 -
Copy modified lines R158-R160 -
Copy modified line R661
| @@ -129,6 +129,19 @@ | ||
| } | ||
|
|
||
| /** | ||
| * Validate that a URL is a safe HTTP(S) URL before using it for navigation. | ||
| */ | ||
| function isSafeHttpUrl(url: string | null | undefined): boolean { | ||
| if (!url) return false; | ||
| try { | ||
| const parsed = new URL(url); | ||
| return parsed.protocol === "http:" || parsed.protocol === "https:"; | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Parse query params for standalone testing mode. | ||
| * URL format: ?wsUrl=ws://localhost:13000/websockify&name=test&password=vncpassword | ||
| */ | ||
| @@ -142,6 +155,9 @@ | ||
| // Derive HTTP URL from WebSocket URL | ||
| const url = wsUrl.replace(/^ws/, "http").replace(/\/websockify$/, ""); | ||
|
|
||
| // Only proceed if the derived URL is a safe HTTP(S) URL | ||
| if (!isSafeHttpUrl(url)) return null; | ||
|
|
||
| return { | ||
| name, | ||
| url, | ||
| @@ -642,7 +658,7 @@ | ||
| }, []); | ||
|
|
||
| const handleOpenInBrowser = useCallback(() => { | ||
| if (extractedInfo?.url) { | ||
| if (extractedInfo?.url && isSafeHttpUrl(extractedInfo.url)) { | ||
| if (app) { | ||
| app.openLink({ url: extractedInfo.url }); | ||
| } else { |
@modelcontextprotocol/ext-apps
@modelcontextprotocol/server-basic-react
@modelcontextprotocol/server-basic-vanillajs
@modelcontextprotocol/server-budget-allocator
@modelcontextprotocol/server-cohort-heatmap
@modelcontextprotocol/server-customer-segmentation
@modelcontextprotocol/server-map
@modelcontextprotocol/server-pdf
@modelcontextprotocol/server-scenario-modeler
@modelcontextprotocol/server-shadertoy
@modelcontextprotocol/server-sheet-music
@modelcontextprotocol/server-system-monitor
@modelcontextprotocol/server-threejs
@modelcontextprotocol/server-transcript
@modelcontextprotocol/server-video-resource
@modelcontextprotocol/server-wiki-explorer
commit: |
Add tools that allow the model to interact programmatically with virtual desktops: - take-screenshot: Capture desktop as PNG image - click: Click at specific coordinates (supports left/middle/right, single/double/triple) - type-text: Type text via keyboard simulation - press-key: Press key combinations (e.g., 'ctrl+c', 'alt+F4', 'Return') - move-mouse: Move cursor to specific position - scroll: Scroll in any direction All tools use xdotool inside the Docker container. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Allows running arbitrary commands inside the Docker container with DISPLAY=:1 so GUI apps appear in the VNC display. Examples: - exec(name, 'firefox', background=true) - Open Firefox - exec(name, 'ls -la ~') - List home directory - exec(name, 'xfce4-terminal', background=true) - Open terminal 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…t result display - Add default values to create-desktop tool (name, variant) - Add default value to view-desktop tool (name) - Improve basic-host ToolResultPanel to render text and images nicely instead of raw JSON for non-UI tool results
…ages - Change container prefix from 'vd-' to 'mcp-apps-vd-' - Improve view-desktop error when desktop not found to suggest create-desktop command with correct arguments
Use ResizeObserver to detect container size changes and call xrandr to resize the desktop resolution accordingly. Includes debouncing and rate-limiting to avoid spamming resize commands.
Set html, body, and #root backgrounds to transparent so the host's rounded iframe corners show properly instead of black corners.
- Add jsdelivr to CSP connect-src for source maps - Fix reconnect by clearing container innerHTML before new connection - Change open-home-folder to open on host machine (not in container) - Add homeFolder path to structuredContent for tooltip - Remove non-working dynamic resize (TigerVNC doesn't support it)
- Fix canvas aspect ratio with object-fit: contain - Re-add resize feature using xrandr -s with predefined modes - Pick best fitting mode from available xrandr resolutions
Add isConnectingRef guard to prevent race conditions where multiple connection attempts would be made, causing the connection to drop.
- Desktop now resizes to exactly match the viewer's dimensions - Uses cvt to generate custom xrandr modes on-the-fly - Canvas fills the frame (no letterboxing since sizes match) - Reduced resize debounce to 500ms for quicker response
- Use noVNC scaleViewport to maintain aspect ratio (prevents distortion) - Dynamic resize using cvt to match container dimensions - Faster 200ms debounce for quicker resize response - Center canvas in container Note: There may be temporary letterboxing until resize completes, but this is preferable to distortion.
- Simplified multiline shell command to single line (avoids escaping issues) - Added logging for resize command and result - Increased timeout to 10 seconds
- Remove minimum size constraints that were forcing 640 width - Use absolute positioning for canvas to avoid layout feedback loop - Observe parent container dimensions directly - Add validateContainerName helper for security The desktop now resizes to match the container size, filling the space with minimal letterboxing (only due to xrandr rounding).
- Add resolveContainerName() to handle both 'my-desktop' and 'mcp-apps-vd-my-desktop' inputs - Update getDesktop() and shutdownDesktop() to use resolver - Update all tools to use desktop.name (resolved) for docker exec - Default view-desktop name to 'my-desktop' (without prefix) Users can now use either: - Logical name: 'my-desktop' - Full name: 'mcp-apps-vd-my-desktop'
- Capture canvas screenshots every 2 seconds when connected - Compare with previous screenshot to avoid sending duplicates - Send via app.updateModelContext() for model context awareness - Silently ignore errors if host doesn't support the feature
- Use JPEG instead of PNG (5-10x smaller) - Use hash for deduplication instead of full string comparison - Check host capabilities before starting screenshot interval - Disable after 3 consecutive failures (backoff) - Use getHostCapabilities() for proper capability check
Replace deprecated RESOURCE_URI_META_KEY constant with the new nested _meta.ui.resourceUri structure as done in other examples.
c9763a7 to
4bc85b6
Compare
…urning from createDesktop - Poll container state after docker run to verify it's running - Fail fast with logs if container exits immediately - Timeout after 30 seconds with helpful error message
…turning from view-desktop - Add waitForVncReady() to poll HTTP endpoint until responsive - view-desktop tool now fails with clear message if VNC not ready - Prevents App UI from loading when endpoint isn't reachable
TigerVNC doesn't support creating custom modes via xrandr --newmode (returns BadName error). Instead, match container size to the closest predefined mode that fits. - Add AVAILABLE_MODES list with TigerVNC's predefined resolutions - findBestMode() selects largest mode that fits within container - Falls back to smallest mode if container is too small (noVNC scales)
…ution matching TigerVNC doesn't support xrandr --newmode at runtime, but it does support starting with a custom -geometry. The new resize-desktop tool restarts VNC with the exact requested dimensions. - Add resize-desktop server tool (visible only to apps) - Update App resize logic to use exact resize when predefined modes don't fit well - If a predefined mode fills < 85% of container, restart VNC with exact size - noVNC will auto-reconnect after VNC restart
…andr Restarting VNC in Docker breaks dbus and the desktop environment (XFCE). The resize-desktop tool is kept for manual use but the App now only uses xrandr to switch between predefined modes. This is less disruptive and always keeps the desktop running.
Resolve conflicts: - package.json: Keep dind scripts and add build step to docker:update - examples/basic-host/src/index.tsx: Keep enhanced ToolResultView with content blocks, add back JsonBlock component
Summary
Features
Test plan
ENABLE_DOCKER_TESTS=1env var):npm run test:e2e:docker:dind🤖 Generated with Claude Code