用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/Aradotso/mcp-skills --skill excalidraw-mcp-server命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
MCP server that enables LLMs to query and analyze PostgreSQL databases through a controlled interface with read/write capabilities.
Use local-mcp to enable AI agents to safely read, write, and execute commands on local files with sandboxed permissions
Run MCPX MCP Runtime to connect AI clients to local development environments with workspace management, source control, changesets, and terminal execution
基于 SOC 职业分类
正在显示 SKILL.md
| name | excalidraw-mcp-server |
| description | Build and deploy MCP servers that stream interactive Excalidraw diagrams with smooth viewport control |
| triggers | ["create an excalidraw diagram in my MCP server","add excalidraw visualization to this tool","stream a hand-drawn diagram using MCP","integrate excalidraw with model context protocol","build an interactive diagram server","render excalidraw in my AI app","create MCP app with diagrams","add visual whiteboard to my MCP tool"] |
Skill by ara.so — MCP Skills collection.
This skill teaches AI agents how to build MCP servers that return interactive Excalidraw diagrams using the MCP Apps extension. Excalidraw MCP enables streaming hand-drawn style diagrams directly into AI chat interfaces like Claude Desktop, ChatGPT, VS Code, and Goose.
The Excalidraw MCP server provides:
Use the hosted version at https://mcp.excalidraw.com:
{
"mcpServers": {
"excalidraw": {
"url": "https://mcp.excalidraw.com"
}
}
}
From source:
git clone https://github.com/excalidraw/excalidraw-mcp.git
cd excalidraw-mcp-app
pnpm install && pnpm run build
Configure in Claude Desktop (~/Library/Application Support/Claude/claude_desktop_config.json):
{
"mcpServers": {
"excalidraw": {
"command": "node",
"args": ["/path/to/excalidraw-mcp-app/dist/index.js", "--stdio"]
}
}
}
From release:
excalidraw-mcp-app.mcpb from releasesMCP Apps is an official extension that lets servers return interactive HTML instead of just text. The response format:
{
content: [
{
type: "resource",
resource: {
uri: "excalidraw://diagram",
mimeType: "text/html",
text: "<html>...</html>"
}
}
]
}
Excalidraw diagrams are JSON with elements and appState:
interface ExcalidrawScene {
type: "excalidraw";
version: 2;
source: string;
elements: ExcalidrawElement[];
appState: {
viewBackgroundColor: string;
currentItemFontFamily?: number;
// ... viewport state
};
files?: Record<string, BinaryFileData>;
}
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
const server = new Server(
{
name: "excalidraw-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "draw_diagram",
description: "Create an interactive Excalidraw diagram",
inputSchema: {
type: "object",
properties: {
elements: {
type: "array",
description: "Excalidraw elements (rectangles, arrows, text, etc.)",
},
description: {
type: "string",
description: "What the diagram shows",
},
},
required: ["elements"],
},
},
],
}));
server.(, (request) => {
(request.. === ) {
{ elements, description } = request.. ;
html = (elements, description);
{
: [
{
: ,
: {
: ,
: ,
: html,
},
},
],
};
}
();
});
transport = ();
server.(transport);
function generateExcalidrawHTML(
elements: any[],
description?: string
): string {
const scene = {
type: "excalidraw",
version: 2,
source: "mcp-server",
elements: elements,
appState: {
viewBackgroundColor: "#ffffff",
currentItemFontFamily: 1,
},
files: {},
};
const sceneJSON = JSON.stringify(scene);
return `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${description || "Excalidraw Diagram"}</title>
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@excalidraw/excalidraw/dist/excalidraw.production.min.js"></script>
<style>
body { margin: 0; padding: 0; overflow: hidden; }
#app { width: 100vw; height: 100vh; }
</style>
</head>
<body>
<div id="app"></div>
<script>
const { Excalidraw } = ExcalidrawLib;
const scene = ${sceneJSON};
const App = () => {
return React.createElement(Excalidraw, {
initialData: scene,
viewModeEnabled: true,
zenModeEnabled: false,
gridModeEnabled: false,
});
};
ReactDOM.render(
React.createElement(App),
document.getElementById('app')
);
</script>
</body>
</html>`;
}
Rectangle:
{
id: "rect-1",
type: "rectangle",
x: 100,
y: 100,
width: 200,
height: 150,
strokeColor: "#1e1e1e",
backgroundColor: "#ffc9c9",
fillStyle: "hachure",
strokeWidth: 2,
roughness: 1,
opacity: 100,
angle: 0,
roundness: { type: 3 },
seed: 12345,
version: 1,
versionNonce: 1,
isDeleted: false,
groupIds: [],
boundElements: null,
}
Text:
{
id: "text-1",
type: "text",
x: 150,
y: 150,
width: 100,
height: 25,
text: "Hello World",
fontSize: 20,
fontFamily: 1, // 1=Virgil, 2=Helvetica, 3=Cascadia
textAlign: "center",
verticalAlign: "middle",
strokeColor: "#1e1e1e",
backgroundColor: "transparent",
fillStyle: "hachure",
strokeWidth: 2,
roughness: 1,
opacity: 100,
angle: 0,
seed: 12346,
version: 1,
versionNonce: 1,
isDeleted: false,
groupIds: [],
containerId: null,
originalText: "Hello World",
}
Arrow:
{
id: "arrow-1",
type: "arrow",
x: 300,
y: 175,
width: 150,
height: 0,
points: [[0, 0], [150, 0]],
strokeColor: "#1e1e1e",
backgroundColor: "transparent",
fillStyle: "hachure",
strokeWidth: 2,
roughness: 1,
opacity: 100,
angle: 0,
startBinding: { elementId: "rect-1", focus: 0, gap: 1 },
endBinding: { elementId: "rect-2", focus: 0, gap: 1 },
startArrowhead: null,
endArrowhead: "arrow",
seed: 12347,
version: 1,
versionNonce: 1,
isDeleted: false,
groupIds: [],
}
Ellipse:
{
id: "ellipse-1",
type: "ellipse",
x: 500,
y: 100,
width: 120,
height: 120,
strokeColor: "#2f9e44",
backgroundColor: "#d3f9d8",
fillStyle: "solid",
strokeWidth: 2,
roughness: 1,
opacity: 100,
angle: 0,
seed: 12348,
version: 1,
versionNonce: 1,
isDeleted: false,
groupIds: [],
boundElements: null,
}
function createArchitectureDiagram(
components: { name: string; type: "user" | "server" | "database" }[]
): any[] {
const elements: any[] = [];
let xOffset = 100;
components.forEach((comp, i) => {
const id = `comp-${i}`;
const y = comp.type === "user" ? 100 : comp.type === "server" ? 300 : 500;
// Component box
elements.push({
id,
type: "rectangle",
x: xOffset,
y,
width: 150,
height: 80,
strokeColor: "#1e1e1e",
backgroundColor: comp.type === "database" ? "#ffd43b" : "#a5d8ff",
fillStyle: "hachure",
strokeWidth: 2,
roughness: 1,
opacity: 100,
: ,
: { : },
: .() * ,
: ,
: ,
: ,
: [],
: ,
});
elements.({
: ,
: ,
: xOffset + ,
: y + ,
: ,
: ,
: comp.,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: .() * ,
: ,
: ,
: ,
: [],
: id,
: comp.,
});
(i < components. - ) {
elements.({
: ,
: ,
: xOffset + ,
: y + ,
: ,
: ,
: [[, ], [, ]],
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: { : id, : , : },
: ,
: ,
: ,
: .() * ,
: ,
: ,
: ,
: [],
});
}
xOffset += ;
});
elements;
}
For progressive rendering, send multiple tool responses:
async function streamDiagram(
initialElements: any[],
updates: any[][]
): Promise<void> {
// Send initial state
await sendToolResponse({
content: [{
type: "resource",
resource: {
uri: "excalidraw://stream-1",
mimeType: "text/html",
text: generateExcalidrawHTML(initialElements),
},
}],
});
// Stream updates
for (const update of updates) {
await new Promise(resolve => setTimeout(resolve, 500));
const allElements = [...initialElements, ...update];
await sendToolResponse({
content: [{
type: "resource",
resource: {
uri: "excalidraw://stream-1", // Same URI to update
mimeType: "text/html",
text: generateExcalidrawHTML(allElements),
},
}],
});
}
}
function generateExcalidrawHTMLWithViewport(
elements: any[],
viewport: { x: number; y: number; zoom: number }
): string {
const scene = {
type: "excalidraw",
version: 2,
source: "mcp-server",
elements,
appState: {
viewBackgroundColor: "#ffffff",
scrollX: viewport.x,
scrollY: viewport.y,
zoom: { value: viewport.zoom },
},
files: {},
};
// ... rest of HTML generation
}
Create api/mcp.ts:
import type { VercelRequest, VercelResponse } from "@vercel/node";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
const server = new Server(
{
name: "excalidraw-server",
version: "1.0.0",
},
{
capabilities: { tools: {} },
}
);
// ... set up handlers (same as above)
export default async function handler(
req: VercelRequest,
res: VercelResponse
) {
const transport = new SSEServerTransport("/message", res);
await server.connect(transport);
await transport.handleRequest(req);
}
Deploy:
vercel --prod
Your server will be at https://your-project.vercel.app/api/mcp.
FROM node:20-alpine
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install --frozen-lockfile
COPY . .
RUN pnpm run build
CMD ["node", "dist/index.js", "--stdio"]
docker build -t excalidraw-mcp .
docker run -i excalidraw-mcp
Problem: Diagram appears blank.
Solution: Ensure all required element properties are present:
{
id: string, // Must be unique
type: string, // "rectangle", "ellipse", "arrow", "text", etc.
x: number, // Position
y: number,
width: number, // Dimensions
height: number,
seed: number, // For roughness randomization
version: number, // Version tracking
versionNonce: number, // Change detection
isDeleted: boolean, // Soft delete flag
groupIds: string[], // Group membership
}
Problem: Arrows don't connect to shapes.
Solution: Ensure startBinding and endBinding reference valid element IDs:
{
type: "arrow",
startBinding: {
elementId: "rect-1", // Must match existing element
focus: 0, // -1 to 1, position on edge
gap: 1, // Distance from edge
},
endBinding: {
elementId: "rect-2",
focus: 0,
gap: 1,
},
}
Problem: Text doesn't appear inside rectangles.
Solution: Set containerId to bind text to container:
{
type: "text",
containerId: "rect-1", // ID of containing rectangle
textAlign: "center",
verticalAlign: "middle",
}
Problem: Updated diagrams don't replace previous ones.
Solution: Use the same uri for all updates:
const diagramURI = `excalidraw://diagram-${sessionId}`;
// All updates must use this same URI
resource: {
uri: diagramURI,
mimeType: "text/html",
text: generateExcalidrawHTML(updatedElements),
}
Problem: Client can't connect to Vercel MCP endpoint.
Solution: Add CORS headers in vercel.json:
{
"headers": [
{
"source": "/api/mcp",
"headers": [
{ "key": "Access-Control-Allow-Origin", "value": "*" },
{ "key": "Access-Control-Allow-Methods", "value": "GET,POST,OPTIONS" },
{ "key": "Access-Control-Allow-Headers", "value": "Content-Type" }
]
}
]
}
crypto.randomUUID() or timestamps--stdio mode before deploying remotely