Microsoft Paint MCP Server

Microsoft Paint MCP Server

MCP server that automates Microsoft Paint on Windows, offering tools to draw freehand strokes, polylines, and logarithmic spirals through Win32 API calls.

Category
访问服务器

README

MCP Server for Drawing in Microsoft Paint from Node.js

English | Español

This project exposes MCP (Model Context Protocol) tools that open Microsoft Paint and draw automatically from Node.js and TypeScript.

Included tools:

  • paint_draw_freehand
  • paint_draw_polyline
  • paint_draw_logarithmic_spiral

These tools automate Microsoft Paint through the Win32 API (user32.dll, shell32.dll) via Koffi.

Important: Paint automation only works on Windows. This is an educational proof of concept. Window interaction is performed with Win32 calls from Node.js through Koffi. It does not use RobotJS, Playwright, Puppeteer, AutoHotkey, or screen capture / visual analysis.

Requirements

  • Windows 10 or 11 (64-bit)
  • Node.js 18 or later (tested with Node 24)
  • Microsoft Paint installed

Installation

npm install

Koffi installs a native binary. If your npm setup restricts scripts, approve Koffi explicitly:

npm approve-scripts koffi

Project Structure

Light hexagonal architecture: the domain is pure and does not know about MCP or Win32. The adapters live under src/infrastructure/. Composition happens in src/server.ts.

src/
  server.ts                        # Composition root
  domain/
    drawing.ts                     # Drawing types, PaintPort, PaintWindow
    figures.ts                     # Pure math helpers for figures
  infrastructure/
    win32/
      user32.ts                    # user32.dll bindings and constants
      shell.ts                     # shell32.dll binding (ShellExecuteW)
      process.ts                   # Generic Windows helpers
      paint.ts                     # Win32 Paint driver implementing PaintPort
    mcp/
      schemas.ts                   # Shared zod schemas
      errors.ts                    # MCP tool error formatting
      registry.ts                  # Registers all MCP operations
      operations/
        freehand.operation.ts
        polyline.operation.ts
        logarithmic-spiral.operation.ts
test/
  helpers.mjs                      # MCP client helpers + spiral generators
  logarithmic-spiral.test.mjs
  polyline.test.mjs
  freehand.test.mjs

Dependency flow:

src/server.ts -> infrastructure/mcp/*
                      |
                      v
               domain/drawing.ts <- infrastructure/win32/paint.ts
                      ^
                      |
               domain/figures.ts

Running

Development:

npm run dev

Build and run:

npm run build
npm start

Sequence Diagram

End-to-end pipeline from an MCP call to actual drawing in Paint:

sequenceDiagram
    autonumber
    participant C as MCP Client / Inspector
    participant S as src/server.ts
    participant O as MCP Operation
    participant P as PaintPort / Win32 Driver
    participant W as Win32 / Shell / user32
    participant M as Paint Window

    C->>S: callTool(name, arguments)
    S->>O: Registered tool handler
    O->>P: paint.createWindow()

    alt No Paint window is open
        P->>W: spawnApplication("mspaint")
        W-->>P: PID
        P->>W: waitForWindowByPid(pid)
    else Paint is already open
        P->>W: enumerateWindows()
        P->>W: spawnApplication("mspaint")
        P->>W: waitForNewPaintWindow(before, 5s)
        alt mspaint.exe does not create a new window
            P->>W: ShellExecuteW(Paint AUMID)
            P->>W: waitForNewPaintWindow(before, 5s)
        end
    end

    W-->>P: WindowInfo (HWND, PID, title, class)
    P->>M: maximizeWindow + bringWindowToFront
    P->>M: wait PAINT_READY_DELAY_MS
    P-->>O: PaintWindow

    alt drawPolyline(points)
        O->>P: window.drawPolyline(points, options)
        P->>M: validate and convert canvas -> client -> screen
        opt skipToolSelection === false
            P->>M: click Pencil tool
        end
        P->>W: SetCursorPos + SendInput(single drag)
    else drawFreehand(strokes)
        O->>P: window.drawFreehand(strokes, options)
        P->>M: validate and convert canvas -> client -> screen
        opt skipToolSelection === false
            P->>M: click Pencil tool
        end
        loop one drag per stroke
            P->>W: SetCursorPos + SendInput(drag)
        end
    end

    P-->>O: structured result
    O-->>S: content + structuredContent
    S-->>C: MCP response

Quick reading:

  • MCP clients never talk to Win32 directly
  • each operation creates its own PaintWindow
  • the Win32 driver decides how to open or create the new Paint window
  • actual automation happens through Win32 APIs such as ShellExecuteW, window enumeration, SetCursorPos, and SendInput
  • tools return normal MCP responses with structuredContent

Adding a New Operation

Each MCP operation lives in its own *.operation.ts file under src/infrastructure/mcp/operations/.

Typical flow:

  1. Add a pure figure helper to src/domain/figures.ts if needed.
  2. Create src/infrastructure/mcp/operations/<name>.operation.ts.
  3. Define input with zod schemas.
  4. In the handler, call paint.createWindow() and then window.drawPolyline(...) or window.drawFreehand(...).
  5. Register the operation in src/infrastructure/mcp/registry.ts.

Minimal example:

import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { PaintPort } from "../../domain/drawing.js";
import { logarithmicSpiral } from "../../domain/figures.js";
import { toolErrorResult } from "../errors.js";

export function registerLogarithmicSpiral(
  server: McpServer,
  paint: PaintPort,
): void {
  server.registerTool(
    "paint_draw_logarithmic_spiral",
    { title: "Logarithmic Spiral", description: "...", inputSchema: {} },
    async () => {
      try {
        const points = logarithmicSpiral(SPIRAL_PARAMS);
        const window = await paint.createWindow();
        const result = await window.drawPolyline(points, { stepDelayMs: 8 });
        return {
          content: [{ type: "text", text: "Done." }],
          structuredContent: result,
        };
      } catch (error: unknown) {
        return toolErrorResult("paint_draw_logarithmic_spiral", error);
      }
    },
  );
}

MCP Inspector

npm run inspect

Start with paint_draw_logarithmic_spiral, then try paint_draw_freehand and paint_draw_polyline.

Tests

Integration tests use Node's built-in test runner and draw on real Paint windows, so they move the real mouse and depend on the active Windows desktop session.

Even though each operation creates its own Paint window, tests must run sequentially because they share the real mouse, Paint process, and Windows focus. That is why npm test uses --test-concurrency=1.

npm run build
npm test

Run a single test:

node --test --test-concurrency=1 test/polyline.test.mjs

Tool Behavior

paint_draw_logarithmic_spiral

Zero-argument example operation. It draws a logarithmic spiral r = 1.1^theta for 6 turns. It is the fastest way to verify the server from MCP Inspector.

paint_draw_freehand

Freehand drawing: one or more strokes, each stroke drawn with a single mouse drag.

Parameters:

  • strokes: 1-100 strokes, each as { points: [{x, y}, ...] }, 2-1000 points per stroke
  • stepDelayMs: integer, 0-200, default 10
  • skipToolSelection: optional boolean; false selects the Pencil tool before drawing

Default Inspector payload:

{
  "strokes": [
    { "points": [{"x": 100, "y": 100}, {"x": 200, "y": 300}, {"x": 300, "y": 100}, {"x": 400, "y": 300}, {"x": 500, "y": 100}] },
    { "points": [{"x": 550, "y": 300}, {"x": 650, "y": 100}] }
  ],
  "stepDelayMs": 10
}

paint_draw_polyline

Draws a connected polyline with a single drag. Useful for curves, spirals, and generated figures.

Parameters:

  • points: 2-1000 {x, y} points
  • stepDelayMs: integer, 0-200, default 10
  • skipToolSelection: optional boolean; false selects the Pencil tool before drawing

Default Inspector payload:

{
  "points": [{"x": 200, "y": 100}, {"x": 600, "y": 100}, {"x": 600, "y": 500}, {"x": 200, "y": 500}],
  "stepDelayMs": 10
}

Paint Window Lifecycle

Each tool call creates its own Paint window and returns metadata including:

  • windowHandle
  • windowTitle
  • processId
  • createdBy

createdBy can be:

  • opened: Paint was not open, so a fresh window was opened
  • launched: Paint was already open and mspaint.exe created a new window
  • shell: mspaint.exe did not create a new window, so ShellExecuteW was used with the Paint AUMID

Internal drawing pipeline:

  1. paint.createWindow()
  2. Maximize the window
  3. Bring it to the foreground
  4. Wait PAINT_READY_DELAY_MS so the canvas is actually ready
  5. Convert canvas coordinates to client coordinates using CANVAS_ORIGIN
  6. Validate bounds
  7. Convert to screen coordinates
  8. Draw with SetCursorPos and SendInput

Win32 APIs Used

  • EnumWindows
  • GetWindowTextW
  • GetClassNameW
  • GetWindowThreadProcessId
  • GetForegroundWindow
  • IsWindow
  • IsWindowVisible
  • IsIconic
  • SetForegroundWindow
  • ShowWindow
  • AttachThreadInput
  • GetClientRect
  • ClientToScreen
  • SetCursorPos
  • GetSystemMetrics
  • SetProcessDpiAwarenessContext
  • SendInput
  • ShellExecuteW

Safety and Validation

  • validates that the HWND still exists before using it
  • rejects negative coordinates
  • rejects points outside the Paint client area
  • limits stepDelayMs to 0-200
  • limits points and strokes to controlled ranges
  • returns a warning if Windows does not allow the window to reach the foreground

Limitations

  • Windows only
  • moves the real mouse during drawing
  • depends on Windows foreground restrictions and an interactive desktop session
  • uses hardcoded layout offsets measured on a specific modern Paint build
  • optional Pencil selection is coordinate-based and less reliable than drawing with the already active tool
  • mspaint.exe can behave like a UWP stub on Windows 11, so the driver may need the ShellExecuteW fallback
  • Paint windows accumulate and must be closed manually

Koffi Notes

  • HWND and HANDLE are treated as 64-bit pointers and represented as BigInt
  • INPUT / MOUSEINPUT must match the exact x64 layout
  • EnumWindows uses a transient Koffi callback that is only valid during the call

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选