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 +21 -0
- package/README.md +95 -2
- package/package.json +33 -4
- package/print.ps1 +76 -0
- package/server.js +98 -0
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
|
-
#
|
|
1
|
+
# windows-print-mcp
|
|
2
2
|
|
|
3
|
-
|
|
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": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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());
|