windows-print-mcp 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 lplplp101
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,96 @@
1
- # Temporary Holding Version
1
+ # windows-print-mcp
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ An MCP server that lets Claude (or any MCP client) **print PDF files on your Windows PC**.
4
+
5
+ > 한국어 설명은 [아래](#한국어)에 있습니다.
6
+
7
+ Ask things like *"Print the PDF in my Downloads folder, pages 1-3, two copies, black and white"* and it goes straight to your printer.
8
+
9
+ ## Tools
10
+
11
+ | Tool | What it does |
12
+ | --- | --- |
13
+ | `list_printers` | Lists installed printers and marks the default one |
14
+ | `print_pdf` | Prints a PDF. Options: `printer`, `copies` (1–20), `pages` (e.g. `1-3,5`), `monochrome`, `duplex` (long edge) |
15
+
16
+ ## Requirements
17
+
18
+ - Windows 10 or 11
19
+ - [Node.js](https://nodejs.org) 18 or newer
20
+
21
+ ## Install
22
+
23
+ ### Claude Desktop
24
+
25
+ Open **Settings → Developer → Edit Config** and add this to `claude_desktop_config.json`
26
+ (if `mcpServers` already exists, add only the `"print"` entry inside it):
27
+
28
+ ```json
29
+ {
30
+ "mcpServers": {
31
+ "print": {
32
+ "command": "npx",
33
+ "args": ["-y", "windows-print-mcp"]
34
+ }
35
+ }
36
+ }
37
+ ```
38
+
39
+ Fully quit Claude Desktop (tray icon → Quit) and start it again.
40
+
41
+ ### Claude Code
42
+
43
+ ```bash
44
+ claude mcp add print -- npx -y windows-print-mcp
45
+ ```
46
+
47
+ ### Other clients (Cursor, VS Code, …)
48
+
49
+ Use command `npx` with args `-y windows-print-mcp` over stdio.
50
+
51
+ ## How it works
52
+
53
+ Each page is rendered with the built-in Windows PDF engine (`Windows.Data.Pdf`, 300 DPI) and sent to the printer through .NET `System.Drawing.Printing`. No extra PDF viewer needs to be installed.
54
+
55
+ ## Notes
56
+
57
+ - Only `.pdf` files can be printed. Paths can be absolute (`C:\Users\me\Downloads\a.pdf`) or start with `~`.
58
+ - Duplex only works if the printer supports automatic two-sided printing.
59
+ - Printing uses real paper and ink — your MCP client will normally ask before calling `print_pdf`.
60
+
61
+ ## License
62
+
63
+ MIT
64
+
65
+ ---
66
+
67
+ ## 한국어
68
+
69
+ Claude가 **내 Windows PC에 연결된 프린터로 PDF를 바로 인쇄**할 수 있게 해 주는 MCP 커넥터입니다.
70
+ "다운로드 폴더에 있는 PDF 1~3쪽만 흑백으로 2부 뽑아 줘"처럼 말하면 됩니다.
71
+
72
+ ### 도구
73
+
74
+ - `list_printers` : 설치된 프린터 목록과 기본 프린터
75
+ - `print_pdf` : PDF 인쇄 (프린터, 매수, 쪽 범위, 흑백, 양면 선택)
76
+
77
+ ### 설치 (Claude 데스크톱 앱)
78
+
79
+ 1. [Node.js](https://nodejs.org) LTS 버전을 설치합니다.
80
+ 2. Claude 앱 **설정 → 개발자 → 구성 편집**을 눌러 `claude_desktop_config.json`을 엽니다.
81
+ 3. 아래 내용을 넣습니다. 이미 `mcpServers`가 있으면 `"print"` 항목만 그 안에 추가하세요.
82
+
83
+ ```json
84
+ {
85
+ "mcpServers": {
86
+ "print": {
87
+ "command": "npx",
88
+ "args": ["-y", "windows-print-mcp"]
89
+ }
90
+ }
91
+ }
92
+ ```
93
+
94
+ 4. Claude 앱을 완전히 종료(트레이 아이콘 → 종료)했다가 다시 켭니다.
95
+
96
+ 양면 인쇄는 자동 양면 기능이 있는 프린터에서만 됩니다.
package/package.json CHANGED
@@ -1,6 +1,35 @@
1
1
  {
2
2
  "name": "windows-print-mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "MCP server that lets Claude (or any MCP client) print PDF files on a Windows PC",
5
+ "type": "module",
6
+ "main": "server.js",
7
+ "bin": {
8
+ "windows-print-mcp": "server.js"
9
+ },
10
+ "files": [
11
+ "server.js",
12
+ "print.ps1",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "mcpName": "io.github.lplplp101/windows-print-mcp",
17
+ "os": ["win32"],
18
+ "engines": {
19
+ "node": ">=18"
20
+ },
21
+ "keywords": ["mcp", "mcp-server", "model-context-protocol", "claude", "print", "printer", "pdf", "windows"],
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/lplplp101/print-mcp.git"
25
+ },
26
+ "homepage": "https://github.com/lplplp101/print-mcp#readme",
27
+ "bugs": "https://github.com/lplplp101/print-mcp/issues",
28
+ "author": "lplplp101",
29
+ "license": "MIT",
30
+ "dependencies": {
31
+ "@modelcontextprotocol/sdk": "^1.32.1",
32
+ "pdf-to-printer": "^5.8.1",
33
+ "zod": "^3.25.0"
34
+ }
35
+ }
package/print.ps1 ADDED
@@ -0,0 +1,76 @@
1
+ param(
2
+ [Parameter(Mandatory=$true)][string]$Path,
3
+ [string]$Printer = "",
4
+ [int]$Copies = 1,
5
+ [string]$Pages = "",
6
+ [switch]$Mono,
7
+ [switch]$Duplex,
8
+ [int]$Dpi = 300
9
+ )
10
+ $ErrorActionPreference = 'Stop'
11
+ [Console]::OutputEncoding = [Text.Encoding]::UTF8
12
+ Add-Type -AssemblyName System.Runtime.WindowsRuntime
13
+ Add-Type -AssemblyName System.Drawing
14
+ $null = [Windows.Storage.StorageFile, Windows.Storage, ContentType = WindowsRuntime]
15
+ $null = [Windows.Data.Pdf.PdfDocument, Windows.Data.Pdf, ContentType = WindowsRuntime]
16
+ $null = [Windows.Storage.Streams.InMemoryRandomAccessStream, Windows.Storage.Streams, ContentType = WindowsRuntime]
17
+
18
+ $ext = [System.WindowsRuntimeSystemExtensions].GetMethods()
19
+ $asTaskOp = $ext | Where-Object { $_.Name -eq 'AsTask' -and $_.GetParameters().Count -eq 1 -and $_.GetParameters()[0].ParameterType.Name -eq 'IAsyncOperation`1' } | Select-Object -First 1
20
+ $asTaskAct = $ext | Where-Object { $_.Name -eq 'AsTask' -and $_.GetParameters().Count -eq 1 -and $_.GetParameters()[0].ParameterType.Name -eq 'IAsyncAction' } | Select-Object -First 1
21
+ function AwaitOp($op, [Type]$t) { $task = $asTaskOp.MakeGenericMethod($t).Invoke($null, @($op)); $task.Wait() | Out-Null; $task.Result }
22
+ function AwaitAct($op) { $task = $asTaskAct.Invoke($null, @($op)); $task.Wait() | Out-Null }
23
+
24
+ $file = AwaitOp ([Windows.Storage.StorageFile]::GetFileFromPathAsync($Path)) ([Windows.Storage.StorageFile])
25
+ $doc = AwaitOp ([Windows.Data.Pdf.PdfDocument]::LoadFromFileAsync($file)) ([Windows.Data.Pdf.PdfDocument])
26
+ $count = [int]$doc.PageCount
27
+
28
+ $want = New-Object 'System.Collections.Generic.HashSet[int]'
29
+ if ([string]::IsNullOrWhiteSpace($Pages)) { 1..$count | ForEach-Object { [void]$want.Add($_) } }
30
+ else {
31
+ foreach ($part in $Pages.Split(',')) {
32
+ $p = $part.Trim(); if (-not $p) { continue }
33
+ if ($p -match '^(\d+)-(\d+)$') { [int]$Matches[1]..[int]$Matches[2] | ForEach-Object { [void]$want.Add($_) } }
34
+ elseif ($p -match '^\d+$') { [void]$want.Add([int]$p) }
35
+ else { throw "Bad page range: $p" }
36
+ }
37
+ }
38
+
39
+ $images = New-Object 'System.Collections.Generic.List[System.Drawing.Image]'
40
+ for ($i = 0; $i -lt $count; $i++) {
41
+ if (-not $want.Contains($i + 1)) { continue }
42
+ $page = $doc.GetPage([uint32]$i)
43
+ $opt = New-Object Windows.Data.Pdf.PdfPageRenderOptions
44
+ $opt.DestinationWidth = [uint32][math]::Round($page.Size.Width * $Dpi / 96)
45
+ $opt.DestinationHeight = [uint32][math]::Round($page.Size.Height * $Dpi / 96)
46
+ $ras = New-Object Windows.Storage.Streams.InMemoryRandomAccessStream
47
+ AwaitAct ($page.RenderToStreamAsync($ras, $opt))
48
+ $ras.Seek(0)
49
+ $net = [System.IO.WindowsRuntimeStreamExtensions]::AsStreamForRead($ras)
50
+ $ms = New-Object System.IO.MemoryStream
51
+ $net.CopyTo($ms); $ms.Position = 0
52
+ $images.Add([System.Drawing.Image]::FromStream($ms))
53
+ $page.Dispose()
54
+ }
55
+ if ($images.Count -eq 0) { throw "No pages selected (document has $count pages)" }
56
+
57
+ $pd = New-Object System.Drawing.Printing.PrintDocument
58
+ if ($Printer) { $pd.PrinterSettings.PrinterName = $Printer }
59
+ if (-not $pd.PrinterSettings.IsValid) { throw "Printer not found: $Printer" }
60
+ $pd.PrinterSettings.Copies = [int16]$Copies
61
+ $pd.DefaultPageSettings.Color = -not $Mono
62
+ if ($Duplex) { $pd.PrinterSettings.Duplex = [System.Drawing.Printing.Duplex]::Vertical }
63
+ $pd.DocumentName = [IO.Path]::GetFileName($Path)
64
+ $pd.PrintController = New-Object System.Drawing.Printing.StandardPrintController
65
+ $script:idx = 0
66
+ $pd.add_PrintPage({
67
+ param($s, $e)
68
+ $img = $images[$script:idx]
69
+ $pb = $e.PageBounds
70
+ $e.Graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
71
+ $e.Graphics.DrawImage($img, [float](-$e.PageSettings.HardMarginX), [float](-$e.PageSettings.HardMarginY), [float]$pb.Width, [float]$pb.Height)
72
+ $script:idx++
73
+ $e.HasMorePages = ($script:idx -lt $images.Count)
74
+ })
75
+ $pd.Print()
76
+ Write-Output ("OK pages={0} printer={1} copies={2}" -f $images.Count, $pd.PrinterSettings.PrinterName, $Copies)
package/server.js ADDED
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import os from "node:os";
6
+ import { execFile } from "node:child_process";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ if (process.platform !== "win32") {
10
+ console.error("windows-print-mcp only runs on Windows.");
11
+ process.exit(1);
12
+ }
13
+
14
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
15
+ const PS = path.join(process.env.SystemRoot || "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
16
+
17
+ function psPrint(file, { printer, copies, pages, monochrome, duplex }) {
18
+ const args = ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", path.join(HERE, "print.ps1"), "-Path", file, "-Copies", String(copies || 1)];
19
+ if (printer) args.push("-Printer", printer);
20
+ if (pages) args.push("-Pages", pages);
21
+ if (monochrome) args.push("-Mono");
22
+ if (duplex) args.push("-Duplex");
23
+ return new Promise((resolve, reject) => {
24
+ execFile(PS, args, { timeout: 180000, windowsHide: true, encoding: "utf8" }, (err, stdout, stderr) => {
25
+ if (err) reject(new Error((stderr || stdout || err.message).trim()));
26
+ else resolve(stdout.trim());
27
+ });
28
+ });
29
+ }
30
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
31
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
32
+ import { z } from "zod";
33
+
34
+ const require = createRequire(import.meta.url);
35
+ const ptp = require("pdf-to-printer");
36
+
37
+ const text = (s) => ({ content: [{ type: "text", text: s }] });
38
+ const fail = (s) => ({ content: [{ type: "text", text: s }], isError: true });
39
+
40
+ function resolvePdf(p) {
41
+ let f = p.trim().replace(/^"|"$/g, "");
42
+ if (f.startsWith("~")) f = path.join(os.homedir(), f.slice(1));
43
+ f = path.resolve(f);
44
+ if (path.extname(f).toLowerCase() !== ".pdf") throw new Error("Only PDF files can be printed: " + f);
45
+ if (!fs.existsSync(f) || !fs.statSync(f).isFile()) throw new Error("File not found: " + f);
46
+ return f;
47
+ }
48
+
49
+ const server = new McpServer({ name: "windows-print-mcp", version: "1.0.0" });
50
+
51
+ server.registerTool(
52
+ "list_printers",
53
+ {
54
+ title: "List printers",
55
+ description: "List printers installed on this Windows PC and show which one is the default.",
56
+ inputSchema: {},
57
+ },
58
+ async () => {
59
+ try {
60
+ const printers = await ptp.getPrinters();
61
+ const def = await ptp.getDefaultPrinter().catch(() => null);
62
+ if (!printers.length) return text("No printers are installed.");
63
+ const lines = printers.map((p) => `- ${p.name}${def && def.name === p.name ? " (default)" : ""}`);
64
+ return text(lines.join("\n"));
65
+ } catch (e) {
66
+ return fail("Could not list printers: " + e.message);
67
+ }
68
+ }
69
+ );
70
+
71
+ server.registerTool(
72
+ "print_pdf",
73
+ {
74
+ title: "Print PDF",
75
+ description:
76
+ "Send a PDF file on this Windows PC to a printer. Use an absolute Windows path (e.g. C:\\Users\\me\\Downloads\\file.pdf) or ~\\Downloads\\file.pdf. Omit printer to use the default printer.",
77
+ inputSchema: {
78
+ path: z.string().describe("Path to the PDF file on this PC"),
79
+ printer: z.string().optional().describe("Printer name from list_printers; default printer if omitted"),
80
+ copies: z.number().int().min(1).max(20).optional().describe("Number of copies (default 1)"),
81
+ pages: z.string().optional().describe("Page range like '1' or '1-3,5'; all pages if omitted"),
82
+ monochrome: z.boolean().optional().describe("Print in black and white"),
83
+ duplex: z.boolean().optional().describe("Print on both sides (long edge)"),
84
+ },
85
+ },
86
+ async ({ path: p, printer, copies, pages, monochrome, duplex }) => {
87
+ try {
88
+ const file = resolvePdf(p);
89
+ const out = await psPrint(file, { printer, copies, pages, monochrome, duplex });
90
+ const def = printer || (await ptp.getDefaultPrinter().catch(() => null))?.name || "default printer";
91
+ return text(`Print job sent.\nFile: ${file}\nPrinter: ${def}\nCopies: ${copies || 1}\n${out}`);
92
+ } catch (e) {
93
+ return fail("Printing failed: " + e.message);
94
+ }
95
+ }
96
+ );
97
+
98
+ await server.connect(new StdioServerTransport());