excalidraw-mcp-server

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Excalidraw MCP Server Skill

Excalidraw MCP Server 技能

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.
ara.so提供的技能 — MCP技能合集。
本技能教AI Agent如何使用MCP Apps扩展构建可返回交互式Excalidraw图表的MCP服务器。Excalidraw MCP支持将手绘风格的图表直接流式传输到Claude Desktop、ChatGPT、VS Code和Goose等AI聊天界面中。

What It Does

功能介绍

The Excalidraw MCP server provides:
  • Interactive diagram rendering: Returns HTML apps that render Excalidraw canvases
  • Streaming updates: Supports progressive diagram building with viewport control
  • Fullscreen editing: Users can click to edit diagrams in a modal
  • Remote & local deployment: Run as Vercel serverless function or local Node.js process
Excalidraw MCP服务器提供:
  • 交互式图表渲染:返回可渲染Excalidraw画布的HTML应用
  • 流式更新:支持带视口控制的渐进式图表构建
  • 全屏编辑:用户可点击在模态框中编辑图表
  • 远程与本地部署:可作为Vercel无服务器函数运行,也可作为本地Node.js进程运行

Installation

安装

Remote Server (Recommended)

远程服务器(推荐)

Use the hosted version at
https://mcp.excalidraw.com
:
json
{
  "mcpServers": {
    "excalidraw": {
      "url": "https://mcp.excalidraw.com"
    }
  }
}
使用托管版本:
https://mcp.excalidraw.com
json
{
  "mcpServers": {
    "excalidraw": {
      "url": "https://mcp.excalidraw.com"
    }
  }
}

Local Installation

本地安装

From source:
bash
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
):
json
{
  "mcpServers": {
    "excalidraw": {
      "command": "node",
      "args": ["/path/to/excalidraw-mcp-app/dist/index.js", "--stdio"]
    }
  }
}
From release:
  1. Download
    excalidraw-mcp-app.mcpb
    from releases
  2. Double-click to install in Claude Desktop
从源码安装:
bash
git clone https://github.com/excalidraw/excalidraw-mcp.git
cd excalidraw-mcp-app
pnpm install && pnpm run build
在Claude Desktop中配置
~/Library/Application Support/Claude/claude_desktop_config.json
):
json
{
  "mcpServers": {
    "excalidraw": {
      "command": "node",
      "args": ["/path/to/excalidraw-mcp-app/dist/index.js", "--stdio"]
    }
  }
}
从发布版本安装:
  1. 发布页面下载
    excalidraw-mcp-app.mcpb
  2. 双击在Claude Desktop中安装

Core Concepts

核心概念

MCP Apps Extension

MCP Apps扩展

MCP Apps is an official extension that lets servers return interactive HTML instead of just text. The response format:
typescript
{
  content: [
    {
      type: "resource",
      resource: {
        uri: "excalidraw://diagram",
        mimeType: "text/html",
        text: "<html>...</html>"
      }
    }
  ]
}
MCP Apps是官方扩展,允许服务器返回交互式HTML而非纯文本。响应格式如下:
typescript
{
  content: [
    {
      type: "resource",
      resource: {
        uri: "excalidraw://diagram",
        mimeType: "text/html",
        text: "<html>...</html>"
      }
    }
  ]
}

Excalidraw Scene Format

Excalidraw场景格式

Excalidraw diagrams are JSON with
elements
and
appState
:
typescript
interface ExcalidrawScene {
  type: "excalidraw";
  version: 2;
  source: string;
  elements: ExcalidrawElement[];
  appState: {
    viewBackgroundColor: string;
    currentItemFontFamily?: number;
    // ... viewport state
  };
  files?: Record<string, BinaryFileData>;
}
Excalidraw图表是包含
elements
appState
的JSON:
typescript
interface ExcalidrawScene {
  type: "excalidraw";
  version: 2;
  source: string;
  elements: ExcalidrawElement[];
  appState: {
    viewBackgroundColor: string;
    currentItemFontFamily?: number;
    // ... 视口状态
  };
  files?: Record<string, BinaryFileData>;
}

Building an Excalidraw MCP Server

构建Excalidraw MCP服务器

Basic Server Structure

基础服务器结构

typescript
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.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name === "draw_diagram") {
    const { elements, description } = request.params.arguments as any;
    
    const html = generateExcalidrawHTML(elements, description);
    
    return {
      content: [
        {
          type: "resource",
          resource: {
            uri: `excalidraw://diagram-${Date.now()}`,
            mimeType: "text/html",
            text: html,
          },
        },
      ],
    };
  }
  
  throw new Error("Unknown tool");
});

const transport = new StdioServerTransport();
await server.connect(transport);
typescript
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.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name === "draw_diagram") {
    const { elements, description } = request.params.arguments as any;
    
    const html = generateExcalidrawHTML(elements, description);
    
    return {
      content: [
        {
          type: "resource",
          resource: {
            uri: `excalidraw://diagram-${Date.now()}`,
            mimeType: "text/html",
            text: html,
          },
        },
      ],
    };
  }
  
  throw new Error("Unknown tool");
});

const transport = new StdioServerTransport();
await server.connect(transport);

Generating the HTML Response

生成HTML响应

typescript
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>`;
}
typescript
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>`;
}

Creating Excalidraw Elements

创建Excalidraw元素

Rectangle:
typescript
{
  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:
typescript
{
  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:
typescript
{
  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:
typescript
{
  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,
}
矩形:
typescript
{
  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,
}
文本:
typescript
{
  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",
}
箭头:
typescript
{
  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: [],
}
椭圆:
typescript
{
  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,
}

Advanced Patterns

高级模式

Architecture Diagram Generator

架构图生成器

typescript
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,
      angle: 0,
      roundness: { type: 3 },
      seed: Math.random() * 10000,
      version: 1,
      versionNonce: 1,
      isDeleted: false,
      groupIds: [],
      boundElements: null,
    });
    
    // Label
    elements.push({
      id: `text-${i}`,
      type: "text",
      x: xOffset + 10,
      y: y + 30,
      width: 130,
      height: 20,
      text: comp.name,
      fontSize: 16,
      fontFamily: 1,
      textAlign: "center",
      verticalAlign: "middle",
      strokeColor: "#1e1e1e",
      backgroundColor: "transparent",
      fillStyle: "hachure",
      strokeWidth: 2,
      roughness: 1,
      opacity: 100,
      angle: 0,
      seed: Math.random() * 10000,
      version: 1,
      versionNonce: 1,
      isDeleted: false,
      groupIds: [],
      containerId: id,
      originalText: comp.name,
    });
    
    // Arrow to next component
    if (i < components.length - 1) {
      elements.push({
        id: `arrow-${i}`,
        type: "arrow",
        x: xOffset + 75,
        y: y + 80,
        width: 0,
        height: 120,
        points: [[0, 0], [0, 120]],
        strokeColor: "#1e1e1e",
        backgroundColor: "transparent",
        fillStyle: "hachure",
        strokeWidth: 2,
        roughness: 1,
        opacity: 100,
        angle: 0,
        startBinding: { elementId: id, focus: 0, gap: 1 },
        endBinding: null,
        startArrowhead: null,
        endArrowhead: "arrow",
        seed: Math.random() * 10000,
        version: 1,
        versionNonce: 1,
        isDeleted: false,
        groupIds: [],
      });
    }
    
    xOffset += 250;
  });
  
  return elements;
}
typescript
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;
    
    // 组件框
    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,
      angle: 0,
      roundness: { type: 3 },
      seed: Math.random() * 10000,
      version: 1,
      versionNonce: 1,
      isDeleted: false,
      groupIds: [],
      boundElements: null,
    });
    
    // 标签
    elements.push({
      id: `text-${i}`,
      type: "text",
      x: xOffset + 10,
      y: y + 30,
      width: 130,
      height: 20,
      text: comp.name,
      fontSize: 16,
      fontFamily: 1,
      textAlign: "center",
      verticalAlign: "middle",
      strokeColor: "#1e1e1e",
      backgroundColor: "transparent",
      fillStyle: "hachure",
      strokeWidth: 2,
      roughness: 1,
      opacity: 100,
      angle: 0,
      seed: Math.random() * 10000,
      version: 1,
      versionNonce: 1,
      isDeleted: false,
      groupIds: [],
      containerId: id,
      originalText: comp.name,
    });
    
    // 连接到下一个组件的箭头
    if (i < components.length - 1) {
      elements.push({
        id: `arrow-${i}`,
        type: "arrow",
        x: xOffset + 75,
        y: y + 80,
        width: 0,
        height: 120,
        points: [[0, 0], [0, 120]],
        strokeColor: "#1e1e1e",
        backgroundColor: "transparent",
        fillStyle: "hachure",
        strokeWidth: 2,
        roughness: 1,
        opacity: 100,
        angle: 0,
        startBinding: { elementId: id, focus: 0, gap: 1 },
        endBinding: null,
        startArrowhead: null,
        endArrowhead: "arrow",
        seed: Math.random() * 10000,
        version: 1,
        versionNonce: 1,
        isDeleted: false,
        groupIds: [],
      });
    }
    
    xOffset += 250;
  });
  
  return elements;
}

Streaming Diagrams with Updates

带更新的流式图表

For progressive rendering, send multiple tool responses:
typescript
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),
        },
      }],
    });
  }
}
对于渐进式渲染,发送多个工具响应:
typescript
async function streamDiagram(
  initialElements: any[],
  updates: any[][]
): Promise<void> {
  // 发送初始状态
  await sendToolResponse({
    content: [{
      type: "resource",
      resource: {
        uri: "excalidraw://stream-1",
        mimeType: "text/html",
        text: generateExcalidrawHTML(initialElements),
      },
    }],
  });
  
  // 流式发送更新
  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", // 使用相同URI进行更新
          mimeType: "text/html",
          text: generateExcalidrawHTML(allElements),
        },
      }],
    });
  }
}

Viewport Control

视口控制

typescript
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
}
typescript
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: {},
  };
  
  // ... 剩余HTML生成代码
}

Deployment

部署

Vercel Serverless Function

Vercel无服务器函数

Create
api/mcp.ts
:
typescript
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:
bash
vercel --prod
Your server will be at
https://your-project.vercel.app/api/mcp
.
创建
api/mcp.ts
typescript
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: {} },
  }
);

// ... 设置处理器(与上述相同)

export default async function handler(
  req: VercelRequest,
  res: VercelResponse
) {
  const transport = new SSEServerTransport("/message", res);
  await server.connect(transport);
  await transport.handleRequest(req);
}
部署:
bash
vercel --prod
你的服务器地址将是
https://your-project.vercel.app/api/mcp

Docker Container

Docker容器

dockerfile
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"]
bash
docker build -t excalidraw-mcp .
docker run -i excalidraw-mcp
dockerfile
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"]
bash
docker build -t excalidraw-mcp .
docker run -i excalidraw-mcp

Troubleshooting

故障排除

Elements Not Rendering

元素未渲染

Problem: Diagram appears blank.
Solution: Ensure all required element properties are present:
typescript
{
  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
}
问题: 图表显示空白。
解决方案: 确保所有必需的元素属性都存在:
typescript
{
  id: string,           // 必须唯一
  type: string,         // "rectangle", "ellipse", "arrow", "text"等
  x: number,            // 位置
  y: number,
  width: number,        // 尺寸
  height: number,
  seed: number,         // 用于粗糙度随机化
  version: number,      // 版本跟踪
  versionNonce: number, // 变更检测
  isDeleted: boolean,   // 软删除标记
  groupIds: string[],   // 组归属
}

Arrow Bindings Not Working

箭头绑定不生效

Problem: Arrows don't connect to shapes.
Solution: Ensure
startBinding
and
endBinding
reference valid element IDs:
typescript
{
  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,
  },
}
问题: 箭头未连接到形状。
解决方案: 确保
startBinding
endBinding
引用有效的元素ID:
typescript
{
  type: "arrow",
  startBinding: {
    elementId: "rect-1",  // 必须匹配现有元素
    focus: 0,              // -1到1,边缘位置
    gap: 1,                // 与边缘的距离
  },
  endBinding: {
    elementId: "rect-2",
    focus: 0,
    gap: 1,
  },
}

Text Inside Shapes Not Centered

形状内的文本未居中

Problem: Text doesn't appear inside rectangles.
Solution: Set
containerId
to bind text to container:
typescript
{
  type: "text",
  containerId: "rect-1",  // ID of containing rectangle
  textAlign: "center",
  verticalAlign: "middle",
}
问题: 文本未显示在矩形内部。
解决方案: 设置
containerId
将文本绑定到容器:
typescript
{
  type: "text",
  containerId: "rect-1",  // 容器矩形的ID
  textAlign: "center",
  verticalAlign: "middle",
}

Streaming Updates Not Appearing

流式更新未显示

Problem: Updated diagrams don't replace previous ones.
Solution: Use the same
uri
for all updates:
typescript
const diagramURI = `excalidraw://diagram-${sessionId}`;

// All updates must use this same URI
resource: {
  uri: diagramURI,
  mimeType: "text/html",
  text: generateExcalidrawHTML(updatedElements),
}
问题: 更新后的图表未替换之前的图表。
解决方案: 所有更新使用相同的
uri
typescript
const diagramURI = `excalidraw://diagram-${sessionId}`;

// 所有更新必须使用相同的URI
resource: {
  uri: diagramURI,
  mimeType: "text/html",
  text: generateExcalidrawHTML(updatedElements),
}

CORS Errors in Vercel Deployment

Vercel部署中的CORS错误

Problem: Client can't connect to Vercel MCP endpoint.
Solution: Add CORS headers in
vercel.json
:
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" }
      ]
    }
  ]
}
问题: 客户端无法连接到Vercel MCP端点。
解决方案:
vercel.json
中添加CORS头:
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" }
      ]
    }
  ]
}

Best Practices

最佳实践

  1. Generate unique IDs: Use
    crypto.randomUUID()
    or timestamps
  2. Set reasonable viewport: Calculate bounds from element positions
  3. Use consistent colors: Define a palette for visual coherence
  4. Add descriptions: Include metadata for accessibility
  5. Validate elements: Check required properties before rendering
  6. Handle errors gracefully: Return text fallbacks if diagram fails
  7. Test locally first: Use
    --stdio
    mode before deploying remotely
  1. 生成唯一ID:使用
    crypto.randomUUID()
    或时间戳
  2. 设置合理的视口:根据元素位置计算边界
  3. 使用一致的颜色:定义调色板以保证视觉一致性
  4. 添加描述:包含元数据以提升可访问性
  5. 验证元素:渲染前检查必需属性
  6. 优雅处理错误:如果图表生成失败,返回文本替代方案
  7. 先在本地测试:部署到远程前先使用
    --stdio
    模式测试

Resources

资源