Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
5b5e848
feat(examples): add virtual-desktop-server with Docker-based VNC viewer
ochafik Jan 12, 2026
f4d214d
test: add e2e tests for virtual-desktop-server
ochafik Jan 12, 2026
15e2695
style: format virtual-desktop.spec.ts
ochafik Jan 12, 2026
ef1f712
feat(virtual-desktop): add interaction tools for model automation
ochafik Jan 12, 2026
2f0d4d4
test: fix virtual-desktop e2e test assertions and timeouts
ochafik Jan 12, 2026
78730b8
feat(virtual-desktop): add exec tool for running commands in container
ochafik Jan 12, 2026
d679991
Merge branch 'main' into feat/virtual-desktop-server
ochafik Jan 12, 2026
d2a3a9f
feat: add default args to virtual-desktop tools and improve basic-hos…
ochafik Jan 12, 2026
f0e229d
feat(virtual-desktop): use mcp-apps-vd- prefix and improve error mess…
ochafik Jan 12, 2026
caa0fd9
feat(virtual-desktop): auto-resize desktop when app container resizes
ochafik Jan 12, 2026
3616c3b
fix(virtual-desktop): make corners transparent
ochafik Jan 12, 2026
b976be6
fix(virtual-desktop): fix CSP, reconnect, and open-home-folder
ochafik Jan 12, 2026
999a3eb
chore(virtual-desktop): rename data dir to ~/.mcp-virtual-desktops-app
ochafik Jan 12, 2026
b4238d4
fix(virtual-desktop): preserve aspect ratio and re-add resize
ochafik Jan 12, 2026
43ad927
fix(virtual-desktop): prevent duplicate VNC connection attempts
ochafik Jan 12, 2026
7036f63
feat(virtual-desktop): dynamic resize to match viewer dimensions
ochafik Jan 12, 2026
ec3a352
fix(virtual-desktop): improve resize with scaleViewport
ochafik Jan 12, 2026
110c036
fix(virtual-desktop): fix resize command execution
ochafik Jan 12, 2026
f95acf7
fix(virtual-desktop): improve resize to fill container
ochafik Jan 12, 2026
ef43f05
fix(virtual-desktop): accept logical names without prefix
ochafik Jan 12, 2026
cae9d0a
Merge remote-tracking branch 'origin/main' into feat/virtual-desktop-…
ochafik Jan 12, 2026
4189147
feat(virtual-desktop): send periodic screenshots via updateModelContext
ochafik Jan 12, 2026
3d629c3
fix(virtual-desktop): improve screenshot updates efficiency
ochafik Jan 12, 2026
2836c7f
Merge remote-tracking branch 'origin/main' into feat/virtual-desktop-…
ochafik Jan 12, 2026
1f87978
refactor(virtual-desktop): use new _meta.ui.resourceUri format
ochafik Jan 12, 2026
8f60116
Merge remote-tracking branch 'origin/main' into feat/virtual-desktop-…
ochafik Jan 12, 2026
24fe5d7
Merge remote-tracking branch 'origin/main' into feat/virtual-desktop-…
ochafik Jan 12, 2026
f714c98
ci: add virtual-desktop-server to npm publish workflow
ochafik Jan 12, 2026
4bc85b6
Merge remote-tracking branch 'origin/main' into feat/virtual-desktop-…
ochafik Jan 13, 2026
c9763a7
feat(virtual-desktop): add usePrivateState hook for localStorage pers…
ochafik Jan 13, 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
1 change: 1 addition & 0 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ jobs:
- threejs-server
- transcript-server
- video-resource-server
- virtual-desktop-server
- wiki-explorer-server

steps:
Expand Down
26 changes: 26 additions & 0 deletions examples/basic-host/src/index.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -253,3 +253,29 @@
background-color: #ddd;
color: #d00;
}

.toolResultPanel {
display: flex;
flex-direction: column;
gap: 0.5rem;
}

.textBlock {
margin: 0;
padding: 1rem;
border-radius: 4px;
background-color: #f5f5f5;
white-space: pre-wrap;
word-break: break-word;
font-family: monospace;
font-size: 0.9rem;
overflow: auto;
max-height: 400px;
}

.imageBlock {
max-width: 100%;
height: auto;
border-radius: 4px;
border: 1px solid #ddd;
}
33 changes: 33 additions & 0 deletions examples/basic-host/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,39 @@ interface ToolResultPanelProps {
}
function ToolResultPanel({ toolCallInfo }: ToolResultPanelProps) {
const result = use(toolCallInfo.resultPromise);

// Render content blocks nicely instead of raw JSON
if (result.content && Array.isArray(result.content)) {
return (
<div className={styles.toolResultPanel}>
{result.isError && <div className={styles.error}><strong>Error</strong></div>}
{result.content.map((block, i) => {
if (block.type === "text") {
return (
<pre key={i} className={styles.textBlock}>
{block.text}
</pre>
);
}
if (block.type === "image") {
const src = `data:${block.mimeType};base64,${block.data}`;
return (
<img
key={i}
src={src}
alt="Tool result"
className={styles.imageBlock}
/>
);
}
// Fallback for unknown content types
return <JsonBlock key={i} value={block} />;
})}
</div>
);
}

// Fallback for non-standard results
return <JsonBlock value={result} />;
}

Expand Down
14 changes: 14 additions & 0 deletions examples/virtual-desktop-server/mcp-app.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="dark">
<title>Virtual Desktop Viewer</title>
<link rel="stylesheet" href="/src/global.css">
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/mcp-app.tsx"></script>
</body>
</html>
48 changes: 48 additions & 0 deletions examples/virtual-desktop-server/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"name": "@anthropic/mcp-server-virtual-desktop",
"version": "0.1.0",
"type": "module",
"description": "MCP server for managing virtual desktops using LinuxServer webtop containers",
"repository": {
"type": "git",
"url": "https://github.com/modelcontextprotocol/ext-apps",
"directory": "examples/virtual-desktop-server"
},
"license": "MIT",
"main": "server.ts",
"files": [
"server.ts",
"src",
"dist"
],
"scripts": {
"build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build",
"watch": "cross-env INPUT=mcp-app.html vite build --watch",
"serve": "bun server.ts",
"start": "cross-env NODE_ENV=development npm run build && npm run serve",
"dev": "cross-env NODE_ENV=development concurrently 'npm run watch' 'npm run serve'",
"prepublishOnly": "npm run build"
},
"dependencies": {
"@modelcontextprotocol/ext-apps": "^0.3.1",
"@modelcontextprotocol/sdk": "^1.24.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"zod": "^4.1.13"
},
"devDependencies": {
"@types/cors": "^2.8.19",
"@types/express": "^5.0.0",
"@types/node": "^22.0.0",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@vitejs/plugin-react": "^4.3.4",
"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"
}
}
72 changes: 72 additions & 0 deletions examples/virtual-desktop-server/server-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* 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, (err) => {
if (err) {
console.error("Failed to start server:", err);
process.exit(1);
}
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