scanner-mcp 0.1.0__tar.gz
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.
- scanner_mcp-0.1.0/.github/workflows/publish.yml +50 -0
- scanner_mcp-0.1.0/.gitignore +22 -0
- scanner_mcp-0.1.0/LICENSE +21 -0
- scanner_mcp-0.1.0/PKG-INFO +167 -0
- scanner_mcp-0.1.0/README.md +132 -0
- scanner_mcp-0.1.0/docs/PROMOTION.md +89 -0
- scanner_mcp-0.1.0/docs/demo.gif +0 -0
- scanner_mcp-0.1.0/docs/make_demo.py +176 -0
- scanner_mcp-0.1.0/pyproject.toml +53 -0
- scanner_mcp-0.1.0/scanner_mcp/__init__.py +3 -0
- scanner_mcp-0.1.0/scanner_mcp/backends/__init__.py +45 -0
- scanner_mcp-0.1.0/scanner_mcp/backends/base.py +29 -0
- scanner_mcp-0.1.0/scanner_mcp/backends/escl.py +223 -0
- scanner_mcp-0.1.0/scanner_mcp/backends/sane.py +126 -0
- scanner_mcp-0.1.0/scanner_mcp/backends/twain.py +210 -0
- scanner_mcp-0.1.0/scanner_mcp/backends/wia.py +218 -0
- scanner_mcp-0.1.0/scanner_mcp/imaging.py +50 -0
- scanner_mcp-0.1.0/scanner_mcp/models.py +52 -0
- scanner_mcp-0.1.0/scanner_mcp/ocr.py +29 -0
- scanner_mcp-0.1.0/scanner_mcp/server.py +200 -0
- scanner_mcp-0.1.0/test_scan.py +137 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
# Publishes to PyPI whenever you publish a GitHub Release.
|
|
4
|
+
# Uses PyPI Trusted Publishing (OIDC) — no API token needs to be stored as a secret.
|
|
5
|
+
# One-time setup on PyPI: create a "pending publisher" for project `scanner-mcp` with
|
|
6
|
+
# Owner: AminHA1248 Repo: scanner-mcp Workflow: publish.yml Environment: pypi
|
|
7
|
+
# (https://pypi.org/manage/account/publishing/)
|
|
8
|
+
|
|
9
|
+
on:
|
|
10
|
+
release:
|
|
11
|
+
types: [published]
|
|
12
|
+
workflow_dispatch: {}
|
|
13
|
+
|
|
14
|
+
permissions:
|
|
15
|
+
contents: read
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
build:
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
- uses: actions/setup-python@v5
|
|
23
|
+
with:
|
|
24
|
+
python-version: "3.12"
|
|
25
|
+
- name: Build sdist and wheel
|
|
26
|
+
run: |
|
|
27
|
+
python -m pip install --upgrade build
|
|
28
|
+
python -m build
|
|
29
|
+
- name: Check distributions
|
|
30
|
+
run: |
|
|
31
|
+
python -m pip install --upgrade twine
|
|
32
|
+
twine check dist/*
|
|
33
|
+
- uses: actions/upload-artifact@v4
|
|
34
|
+
with:
|
|
35
|
+
name: dist
|
|
36
|
+
path: dist/
|
|
37
|
+
|
|
38
|
+
publish:
|
|
39
|
+
needs: build
|
|
40
|
+
runs-on: ubuntu-latest
|
|
41
|
+
environment: pypi
|
|
42
|
+
permissions:
|
|
43
|
+
id-token: write # required for Trusted Publishing
|
|
44
|
+
steps:
|
|
45
|
+
- uses: actions/download-artifact@v4
|
|
46
|
+
with:
|
|
47
|
+
name: dist
|
|
48
|
+
path: dist/
|
|
49
|
+
- name: Publish to PyPI
|
|
50
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Virtual environments
|
|
2
|
+
.venv/
|
|
3
|
+
venv/
|
|
4
|
+
env/
|
|
5
|
+
|
|
6
|
+
# Python bytecode / caches
|
|
7
|
+
__pycache__/
|
|
8
|
+
*.py[cod]
|
|
9
|
+
*.egg-info/
|
|
10
|
+
.eggs/
|
|
11
|
+
build/
|
|
12
|
+
dist/
|
|
13
|
+
|
|
14
|
+
# Editor / OS
|
|
15
|
+
.vscode/
|
|
16
|
+
.idea/
|
|
17
|
+
.DS_Store
|
|
18
|
+
Thumbs.db
|
|
19
|
+
|
|
20
|
+
# Local scan output (if ever pointed inside the repo)
|
|
21
|
+
Scans/
|
|
22
|
+
*.bak
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Amin Haji-Abolhassani
|
|
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.
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: scanner-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Generic MCP server exposing a scan tool to network (eSCL/AirScan) and USB (WIA/SANE/TWAIN) scanners.
|
|
5
|
+
Project-URL: Homepage, https://github.com/AminHA1248/scanner-mcp
|
|
6
|
+
Project-URL: Repository, https://github.com/AminHA1248/scanner-mcp
|
|
7
|
+
Project-URL: Issues, https://github.com/AminHA1248/scanner-mcp/issues
|
|
8
|
+
Author-email: Amin Haji-Abolhassani <amin.h.a@gmail.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: airscan,claude,escl,mcp,model-context-protocol,mopria,ocr,sane,scanner,scanning,twain,wia
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: MacOS
|
|
16
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
17
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Multimedia :: Graphics :: Capture :: Scanners
|
|
24
|
+
Classifier: Topic :: Utilities
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Requires-Dist: httpx>=0.27
|
|
27
|
+
Requires-Dist: mcp>=1.2.0
|
|
28
|
+
Requires-Dist: pillow>=10.0
|
|
29
|
+
Requires-Dist: zeroconf>=0.132
|
|
30
|
+
Provides-Extra: ocr
|
|
31
|
+
Requires-Dist: pytesseract>=0.3.10; extra == 'ocr'
|
|
32
|
+
Provides-Extra: twain
|
|
33
|
+
Requires-Dist: pytwain>=2.3; (platform_system == 'Windows') and extra == 'twain'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# scanner-mcp
|
|
37
|
+
|
|
38
|
+

|
|
39
|
+

|
|
40
|
+

|
|
41
|
+

|
|
42
|
+

|
|
43
|
+
|
|
44
|
+
> **Let Claude scan and read paper documents** from any USB or network scanner.
|
|
45
|
+
|
|
46
|
+
<p align="center">
|
|
47
|
+
<img src="https://raw.githubusercontent.com/AminHA1248/scanner-mcp/main/docs/demo.gif" alt="Ask Claude to scan a document; it calls the scan_document tool and reads the page back" width="760">
|
|
48
|
+
</p>
|
|
49
|
+
|
|
50
|
+
A **generic MCP server** that exposes a scanning tool to Claude (and any other MCP
|
|
51
|
+
client). It works with **network scanners** through the standard **eSCL / AirScan /
|
|
52
|
+
Mopria** protocol and with **USB scanners** through the platform driver stack —
|
|
53
|
+
**WIA** on Windows and **SANE** on Linux/macOS. No vendor-specific driver code.
|
|
54
|
+
|
|
55
|
+
When Claude needs to read a paper document, it can call `scan_document`, and the page
|
|
56
|
+
image is returned inline so Claude can read it directly (optionally OCR'd to text, or
|
|
57
|
+
saved as PDF).
|
|
58
|
+
|
|
59
|
+
## What it exposes
|
|
60
|
+
|
|
61
|
+
| Tool | Purpose |
|
|
62
|
+
|------|---------|
|
|
63
|
+
| `list_scanners` | Discover every scanner reachable from this machine (network + USB). |
|
|
64
|
+
| `scan_document` | Scan a page/stack and return it as inline images, a saved file, and/or OCR text. |
|
|
65
|
+
|
|
66
|
+
Backends are auto-detected and degrade gracefully — the same server runs on any OS and
|
|
67
|
+
lights up whatever scanners it can reach:
|
|
68
|
+
|
|
69
|
+
- **eSCL** (`_uscan._tcp` mDNS) — driverless network MFPs/scanners. *Cross-platform.*
|
|
70
|
+
- **WIA** — USB (and some network) scanners on **Windows**, driven via PowerShell COM.
|
|
71
|
+
- **TWAIN** — Windows scanners that expose only a TWAIN data source (older/pro units
|
|
72
|
+
with no usable WIA driver). Optional; needs `pytwain` + a TWAIN DSM (see below).
|
|
73
|
+
- **SANE** (`scanimage`) — USB/network scanners on **Linux** and **macOS**.
|
|
74
|
+
|
|
75
|
+
## Install
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
cd scanner-mcp
|
|
79
|
+
python -m venv .venv
|
|
80
|
+
# Windows: .venv\Scripts\activate
|
|
81
|
+
# macOS/Linux: source .venv/bin/activate
|
|
82
|
+
pip install -e .
|
|
83
|
+
# optional OCR support:
|
|
84
|
+
pip install -e ".[ocr]" # also needs the Tesseract binary installed
|
|
85
|
+
# optional TWAIN backend (Windows only):
|
|
86
|
+
pip install -e ".[twain]" # also needs a TWAIN DSM (see below)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Platform prerequisites:
|
|
90
|
+
|
|
91
|
+
- **Windows**: nothing extra for WIA — it and PowerShell ship with Windows. Install the
|
|
92
|
+
scanner's normal Windows driver so it appears in *Devices*.
|
|
93
|
+
- **TWAIN (optional)**: install `pytwain` (`pip install -e ".[twain]"`) and make sure a
|
|
94
|
+
TWAIN **DSM** is present. 64-bit Python needs `TWAINDSM.dll` (shipped by most TWAIN
|
|
95
|
+
2.x drivers or the TWAIN DSM redistributable); 32-bit Python can use the classic
|
|
96
|
+
`twain_32.dll`. If neither is installed, the TWAIN backend just stays disabled.
|
|
97
|
+
- **Linux**: `sudo apt install sane-utils` (provides `scanimage`).
|
|
98
|
+
- **macOS**: `brew install sane-backends` for USB; network scanners work via eSCL with no extras.
|
|
99
|
+
- **Network scanners**: just be on the same LAN/subnet; mDNS handles discovery.
|
|
100
|
+
|
|
101
|
+
## Connect it to Claude
|
|
102
|
+
|
|
103
|
+
### Claude Desktop
|
|
104
|
+
Edit `claude_desktop_config.json` (Settings → Developer → Edit Config):
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"mcpServers": {
|
|
109
|
+
"scanner": {
|
|
110
|
+
"command": "C:\\Users\\aminh\\scanner-mcp\\.venv\\Scripts\\scanner-mcp.exe"
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
On macOS/Linux use the venv path `/path/to/scanner-mcp/.venv/bin/scanner-mcp`.
|
|
117
|
+
|
|
118
|
+
### Claude Code (CLI)
|
|
119
|
+
```bash
|
|
120
|
+
claude mcp add scanner -- /path/to/scanner-mcp/.venv/bin/scanner-mcp
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Restart the client, then ask Claude: *"List my scanners"* or *"Scan the document on the
|
|
124
|
+
glass and read it."*
|
|
125
|
+
|
|
126
|
+
## How you actually run it
|
|
127
|
+
|
|
128
|
+
You normally **don't launch anything yourself** — Claude Desktop/Code starts the
|
|
129
|
+
`scanner-mcp` server in the background (per the config above) and calls its tools when
|
|
130
|
+
you ask. Running `scanner-mcp` by hand just starts the MCP server, which waits silently
|
|
131
|
+
for JSON-RPC on stdin; it is not an interactive shell.
|
|
132
|
+
|
|
133
|
+
To test the hardware **without Claude**, use the bundled CLI, [`test_scan.py`](test_scan.py):
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
# from the project folder, using the venv's Python
|
|
137
|
+
python test_scan.py --list # discover scanners
|
|
138
|
+
python test_scan.py --dpi 300 # scan (auto-selects if only one)
|
|
139
|
+
python test_scan.py --scanner "<id>" --source adf --format pdf
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Configuration (env vars)
|
|
143
|
+
|
|
144
|
+
| Variable | Default | Meaning |
|
|
145
|
+
|----------|---------|---------|
|
|
146
|
+
| `SCANNER_MCP_SAVE_DIR` | `~/Scans` | Where scans are written. |
|
|
147
|
+
| `SCANNER_MCP_LOG` | `INFO` | Log level. |
|
|
148
|
+
|
|
149
|
+
## `scan_document` options
|
|
150
|
+
|
|
151
|
+
`scanner_id` (from `list_scanners`; auto if only one), `source`
|
|
152
|
+
(`auto`/`platen`/`adf`/`adf-duplex`), `resolution` (DPI), `color_mode`
|
|
153
|
+
(`color`/`gray`/`lineart`), `output_format` (`png`/`jpeg`/`pdf`), `save_dir`,
|
|
154
|
+
`return_image` (inline images for Claude to read), `ocr` (extract text).
|
|
155
|
+
|
|
156
|
+
## Notes & limitations
|
|
157
|
+
|
|
158
|
+
- **eSCL** covers most scanners sold in the last ~decade (anything "AirPrint/AirScan"
|
|
159
|
+
or "Mopria" capable). Older USB-only units go through WIA/SANE instead.
|
|
160
|
+
- PDF output for multi-page WIA scans is assembled with Pillow.
|
|
161
|
+
- HTTPS eSCL devices use self-signed certs, so TLS verification is disabled for them
|
|
162
|
+
(typical for LAN scanners); prefer a trusted network.
|
|
163
|
+
- This server performs local hardware I/O only — it does not send anything to the cloud.
|
|
164
|
+
|
|
165
|
+
## License
|
|
166
|
+
|
|
167
|
+
MIT
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# scanner-mcp
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
> **Let Claude scan and read paper documents** from any USB or network scanner.
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<img src="https://raw.githubusercontent.com/AminHA1248/scanner-mcp/main/docs/demo.gif" alt="Ask Claude to scan a document; it calls the scan_document tool and reads the page back" width="760">
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
A **generic MCP server** that exposes a scanning tool to Claude (and any other MCP
|
|
16
|
+
client). It works with **network scanners** through the standard **eSCL / AirScan /
|
|
17
|
+
Mopria** protocol and with **USB scanners** through the platform driver stack —
|
|
18
|
+
**WIA** on Windows and **SANE** on Linux/macOS. No vendor-specific driver code.
|
|
19
|
+
|
|
20
|
+
When Claude needs to read a paper document, it can call `scan_document`, and the page
|
|
21
|
+
image is returned inline so Claude can read it directly (optionally OCR'd to text, or
|
|
22
|
+
saved as PDF).
|
|
23
|
+
|
|
24
|
+
## What it exposes
|
|
25
|
+
|
|
26
|
+
| Tool | Purpose |
|
|
27
|
+
|------|---------|
|
|
28
|
+
| `list_scanners` | Discover every scanner reachable from this machine (network + USB). |
|
|
29
|
+
| `scan_document` | Scan a page/stack and return it as inline images, a saved file, and/or OCR text. |
|
|
30
|
+
|
|
31
|
+
Backends are auto-detected and degrade gracefully — the same server runs on any OS and
|
|
32
|
+
lights up whatever scanners it can reach:
|
|
33
|
+
|
|
34
|
+
- **eSCL** (`_uscan._tcp` mDNS) — driverless network MFPs/scanners. *Cross-platform.*
|
|
35
|
+
- **WIA** — USB (and some network) scanners on **Windows**, driven via PowerShell COM.
|
|
36
|
+
- **TWAIN** — Windows scanners that expose only a TWAIN data source (older/pro units
|
|
37
|
+
with no usable WIA driver). Optional; needs `pytwain` + a TWAIN DSM (see below).
|
|
38
|
+
- **SANE** (`scanimage`) — USB/network scanners on **Linux** and **macOS**.
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
cd scanner-mcp
|
|
44
|
+
python -m venv .venv
|
|
45
|
+
# Windows: .venv\Scripts\activate
|
|
46
|
+
# macOS/Linux: source .venv/bin/activate
|
|
47
|
+
pip install -e .
|
|
48
|
+
# optional OCR support:
|
|
49
|
+
pip install -e ".[ocr]" # also needs the Tesseract binary installed
|
|
50
|
+
# optional TWAIN backend (Windows only):
|
|
51
|
+
pip install -e ".[twain]" # also needs a TWAIN DSM (see below)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Platform prerequisites:
|
|
55
|
+
|
|
56
|
+
- **Windows**: nothing extra for WIA — it and PowerShell ship with Windows. Install the
|
|
57
|
+
scanner's normal Windows driver so it appears in *Devices*.
|
|
58
|
+
- **TWAIN (optional)**: install `pytwain` (`pip install -e ".[twain]"`) and make sure a
|
|
59
|
+
TWAIN **DSM** is present. 64-bit Python needs `TWAINDSM.dll` (shipped by most TWAIN
|
|
60
|
+
2.x drivers or the TWAIN DSM redistributable); 32-bit Python can use the classic
|
|
61
|
+
`twain_32.dll`. If neither is installed, the TWAIN backend just stays disabled.
|
|
62
|
+
- **Linux**: `sudo apt install sane-utils` (provides `scanimage`).
|
|
63
|
+
- **macOS**: `brew install sane-backends` for USB; network scanners work via eSCL with no extras.
|
|
64
|
+
- **Network scanners**: just be on the same LAN/subnet; mDNS handles discovery.
|
|
65
|
+
|
|
66
|
+
## Connect it to Claude
|
|
67
|
+
|
|
68
|
+
### Claude Desktop
|
|
69
|
+
Edit `claude_desktop_config.json` (Settings → Developer → Edit Config):
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"mcpServers": {
|
|
74
|
+
"scanner": {
|
|
75
|
+
"command": "C:\\Users\\aminh\\scanner-mcp\\.venv\\Scripts\\scanner-mcp.exe"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
On macOS/Linux use the venv path `/path/to/scanner-mcp/.venv/bin/scanner-mcp`.
|
|
82
|
+
|
|
83
|
+
### Claude Code (CLI)
|
|
84
|
+
```bash
|
|
85
|
+
claude mcp add scanner -- /path/to/scanner-mcp/.venv/bin/scanner-mcp
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Restart the client, then ask Claude: *"List my scanners"* or *"Scan the document on the
|
|
89
|
+
glass and read it."*
|
|
90
|
+
|
|
91
|
+
## How you actually run it
|
|
92
|
+
|
|
93
|
+
You normally **don't launch anything yourself** — Claude Desktop/Code starts the
|
|
94
|
+
`scanner-mcp` server in the background (per the config above) and calls its tools when
|
|
95
|
+
you ask. Running `scanner-mcp` by hand just starts the MCP server, which waits silently
|
|
96
|
+
for JSON-RPC on stdin; it is not an interactive shell.
|
|
97
|
+
|
|
98
|
+
To test the hardware **without Claude**, use the bundled CLI, [`test_scan.py`](test_scan.py):
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
# from the project folder, using the venv's Python
|
|
102
|
+
python test_scan.py --list # discover scanners
|
|
103
|
+
python test_scan.py --dpi 300 # scan (auto-selects if only one)
|
|
104
|
+
python test_scan.py --scanner "<id>" --source adf --format pdf
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Configuration (env vars)
|
|
108
|
+
|
|
109
|
+
| Variable | Default | Meaning |
|
|
110
|
+
|----------|---------|---------|
|
|
111
|
+
| `SCANNER_MCP_SAVE_DIR` | `~/Scans` | Where scans are written. |
|
|
112
|
+
| `SCANNER_MCP_LOG` | `INFO` | Log level. |
|
|
113
|
+
|
|
114
|
+
## `scan_document` options
|
|
115
|
+
|
|
116
|
+
`scanner_id` (from `list_scanners`; auto if only one), `source`
|
|
117
|
+
(`auto`/`platen`/`adf`/`adf-duplex`), `resolution` (DPI), `color_mode`
|
|
118
|
+
(`color`/`gray`/`lineart`), `output_format` (`png`/`jpeg`/`pdf`), `save_dir`,
|
|
119
|
+
`return_image` (inline images for Claude to read), `ocr` (extract text).
|
|
120
|
+
|
|
121
|
+
## Notes & limitations
|
|
122
|
+
|
|
123
|
+
- **eSCL** covers most scanners sold in the last ~decade (anything "AirPrint/AirScan"
|
|
124
|
+
or "Mopria" capable). Older USB-only units go through WIA/SANE instead.
|
|
125
|
+
- PDF output for multi-page WIA scans is assembled with Pillow.
|
|
126
|
+
- HTTPS eSCL devices use self-signed certs, so TLS verification is disabled for them
|
|
127
|
+
(typical for LAN scanners); prefer a trusted network.
|
|
128
|
+
- This server performs local hardware I/O only — it does not send anything to the cloud.
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
MIT
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Promotion kit
|
|
2
|
+
|
|
3
|
+
Ready-to-paste copy for getting scanner-mcp in front of people. Nothing here posts
|
|
4
|
+
automatically — pick what you want and I (or you) can submit it.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 1. awesome-mcp-servers list entries
|
|
9
|
+
|
|
10
|
+
**[punkpeye/awesome-mcp-servers](https://github.com/punkpeye/awesome-mcp-servers)**
|
|
11
|
+
(icons: 🐍 Python · 🏠 local · 🪟 Windows · 🐧 Linux · 🍎 macOS). Best-fit section:
|
|
12
|
+
**🖥️ OS Automation**.
|
|
13
|
+
|
|
14
|
+
```markdown
|
|
15
|
+
- [AminHA1248/scanner-mcp](https://github.com/AminHA1248/scanner-mcp) 🐍 🏠 🪟 🐧 🍎 - Let Claude scan and read paper documents from any USB or network scanner (eSCL/AirScan, WIA, SANE, TWAIN)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
**[wong2/awesome-mcp-servers](https://github.com/wong2/awesome-mcp-servers)** (plainer format):
|
|
19
|
+
|
|
20
|
+
```markdown
|
|
21
|
+
- [scanner-mcp](https://github.com/AminHA1248/scanner-mcp) - Scan and read paper documents from any USB or network scanner (eSCL, WIA, SANE, TWAIN)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Directory sites that also accept submissions:** mcp.so, Glama, Smithery, PulseMCP,
|
|
25
|
+
mcpservers.org. Most have a "submit" form and take the same one-line description.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 2. Show HN
|
|
30
|
+
|
|
31
|
+
**Title:**
|
|
32
|
+
```
|
|
33
|
+
Show HN: Scanner MCP – let Claude scan and read paper documents
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Body:**
|
|
37
|
+
```
|
|
38
|
+
I wanted Claude to be able to read a physical document, so I built an MCP server
|
|
39
|
+
that exposes scan tools to any scanner Claude can reach.
|
|
40
|
+
|
|
41
|
+
It's generic on purpose — instead of vendor SDKs it targets the standard interfaces
|
|
42
|
+
each scanner category already exposes:
|
|
43
|
+
- eSCL / AirScan / Mopria for network scanners (driverless, cross-platform)
|
|
44
|
+
- WIA on Windows and SANE on Linux/macOS for USB
|
|
45
|
+
- TWAIN on Windows for older units with no usable WIA driver
|
|
46
|
+
|
|
47
|
+
Ask Claude "scan my document and read it" — it calls scan_document, and the page
|
|
48
|
+
comes back as an inline image Claude reads directly (or OCR'd, or saved as PDF).
|
|
49
|
+
|
|
50
|
+
Honest status: eSCL + WIA are tested on real hardware (a Canon TS3400); SANE and
|
|
51
|
+
TWAIN are written to the standard interfaces but not yet hardware-verified.
|
|
52
|
+
|
|
53
|
+
Repo (MIT): https://github.com/AminHA1248/scanner-mcp
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 3. Reddit — r/ClaudeAI and r/mcp
|
|
59
|
+
|
|
60
|
+
**Title:**
|
|
61
|
+
```
|
|
62
|
+
I built an MCP server that lets Claude scan and read paper documents
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Body:**
|
|
66
|
+
```
|
|
67
|
+
Sharing a weekend project: an MCP server that gives Claude a scan_document tool.
|
|
68
|
+
Put a page on your scanner, ask Claude to "scan and read it," and it reads the page
|
|
69
|
+
back (inline image, or OCR to text, or save a PDF).
|
|
70
|
+
|
|
71
|
+
It's driverless where it can be — eSCL/AirScan for network scanners, WIA for Windows
|
|
72
|
+
USB, SANE for Linux/macOS, TWAIN for older Windows scanners. One pip install, one line
|
|
73
|
+
in your Claude Desktop config.
|
|
74
|
+
|
|
75
|
+
Tested end-to-end on a Canon TS3400. MIT licensed, feedback welcome:
|
|
76
|
+
https://github.com/AminHA1248/scanner-mcp
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Reddit etiquette: post as "I built…", reply to comments, don't cross-post the same
|
|
80
|
+
text to five subs at once. r/ClaudeAI and r/mcp are the on-topic homes.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 4. Sequencing (do this order)
|
|
85
|
+
|
|
86
|
+
1. Publish to PyPI so every link can say `pip install scanner-mcp` (see README/workflow).
|
|
87
|
+
2. Submit the awesome-mcp PRs (durable, targeted traffic).
|
|
88
|
+
3. Post Show HN once, mid-morning US time; then the Reddit posts.
|
|
89
|
+
4. Watch GitHub → Insights → Traffic → Referrers and lean into whatever converts.
|
|
Binary file
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Generate docs/demo.gif — a Claude-chat demo of scanner-mcp.
|
|
2
|
+
|
|
3
|
+
Shows the real way the server is used: you ask Claude, it calls the
|
|
4
|
+
`scan_document` tool, then reads the page back. Built from the actual scan.
|
|
5
|
+
Run: python docs/make_demo.py (needs Pillow; uses the real scan if present).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
14
|
+
|
|
15
|
+
W, H = 860, 560
|
|
16
|
+
TITLEBAR = 34
|
|
17
|
+
PAD = 22
|
|
18
|
+
LH = 25
|
|
19
|
+
FONT_DIR = r"C:\Windows\Fonts"
|
|
20
|
+
|
|
21
|
+
BG = (13, 17, 23)
|
|
22
|
+
BAR = (22, 27, 34)
|
|
23
|
+
BORDER = (48, 54, 61)
|
|
24
|
+
GRAY = (139, 148, 158)
|
|
25
|
+
CYAN = (86, 182, 194)
|
|
26
|
+
GREEN = (86, 211, 100)
|
|
27
|
+
YELLOW = (210, 168, 60)
|
|
28
|
+
WHITE = (230, 237, 243)
|
|
29
|
+
USER_BUBBLE = (35, 66, 120)
|
|
30
|
+
PILL_BG = (26, 31, 39)
|
|
31
|
+
|
|
32
|
+
HERE = Path(__file__).resolve().parent
|
|
33
|
+
SCAN = Path(os.environ.get("DEMO_SCAN", r"C:\Users\aminh\Scans\scan-20260711-145133.png"))
|
|
34
|
+
OUT = HERE / "demo.gif"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def font(size, bold=False, mono=True):
|
|
38
|
+
if mono:
|
|
39
|
+
name = "consolab.ttf" if bold else "consola.ttf"
|
|
40
|
+
else:
|
|
41
|
+
name = "segoeuib.ttf" if bold else "segoeui.ttf"
|
|
42
|
+
try:
|
|
43
|
+
return ImageFont.truetype(os.path.join(FONT_DIR, name), size)
|
|
44
|
+
except Exception:
|
|
45
|
+
return ImageFont.load_default()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
F = font(18, mono=False)
|
|
49
|
+
FB = font(18, bold=True, mono=False)
|
|
50
|
+
FM = font(16) # mono for tool pill
|
|
51
|
+
FT = font(14, bold=True, mono=False)
|
|
52
|
+
|
|
53
|
+
frames: list[Image.Image] = []
|
|
54
|
+
durations: list[int] = []
|
|
55
|
+
|
|
56
|
+
# --- prepare the scanned page (downscaled) --------------------------------
|
|
57
|
+
doc_w = 214
|
|
58
|
+
if SCAN.exists():
|
|
59
|
+
doc = Image.open(SCAN).convert("RGB")
|
|
60
|
+
else:
|
|
61
|
+
doc = Image.new("RGB", (500, 700), (245, 244, 236))
|
|
62
|
+
doc = doc.resize((doc_w, int(doc.height * doc_w / doc.width)))
|
|
63
|
+
|
|
64
|
+
USER_TEXT = "Scan my document and read it"
|
|
65
|
+
bullets = [
|
|
66
|
+
"Upstream → Packloop → Accumulator",
|
|
67
|
+
"2026-06-06 20:03:02 UTC",
|
|
68
|
+
"KPI · Offline processing",
|
|
69
|
+
"Yolo26 + SAM + active",
|
|
70
|
+
"Wedged GPU · 1TB (userid 1000)",
|
|
71
|
+
"C39 > SMB > logs",
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
# --- layout ---------------------------------------------------------------
|
|
75
|
+
ub_y0 = TITLEBAR + PAD
|
|
76
|
+
ub_h = 42
|
|
77
|
+
asst_y = ub_y0 + ub_h + 26
|
|
78
|
+
pill_y = asst_y + 22
|
|
79
|
+
msg_y = pill_y + 42
|
|
80
|
+
doc_x = PAD
|
|
81
|
+
doc_y = msg_y + 34
|
|
82
|
+
doc_h = min(doc.height, H - doc_y - PAD)
|
|
83
|
+
doc = doc.crop((0, 0, doc_w, doc_h))
|
|
84
|
+
tx = doc_x + doc_w + 34
|
|
85
|
+
bullets_y = doc_y + 2
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def base():
|
|
89
|
+
img = Image.new("RGB", (W, H), BG)
|
|
90
|
+
d = ImageDraw.Draw(img)
|
|
91
|
+
d.rectangle([0, 0, W, TITLEBAR], fill=BAR)
|
|
92
|
+
d.line([0, TITLEBAR, W, TITLEBAR], fill=BORDER)
|
|
93
|
+
for i, c in enumerate([(255, 95, 86), (255, 189, 46), (39, 201, 63)]):
|
|
94
|
+
d.ellipse([PAD + i * 22, 11, PAD + i * 22 + 12, 23], fill=c)
|
|
95
|
+
d.text((W // 2 - 26, 9), "Claude", font=FT, fill=GRAY)
|
|
96
|
+
return img, d
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def user_bubble(d, visible):
|
|
100
|
+
full_w = d.textlength(USER_TEXT, font=F)
|
|
101
|
+
bw = full_w + 28
|
|
102
|
+
x1 = W - PAD
|
|
103
|
+
x0 = x1 - bw
|
|
104
|
+
d.rounded_rectangle([x0, ub_y0, x1, ub_y0 + ub_h], radius=12, fill=USER_BUBBLE)
|
|
105
|
+
d.text((x0 + 14, ub_y0 + 9), visible, font=F, fill=WHITE)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def tool_pill(d, state):
|
|
109
|
+
# state: None | "run" | "done"
|
|
110
|
+
if state is None:
|
|
111
|
+
return
|
|
112
|
+
label = "scanner ▸ scan_document"
|
|
113
|
+
tw = d.textlength(label, font=FM)
|
|
114
|
+
x0, y0 = PAD, pill_y
|
|
115
|
+
x1, y1 = x0 + tw + 58, y0 + 30
|
|
116
|
+
d.rounded_rectangle([x0, y0, x1, y1], radius=8, fill=PILL_BG, outline=BORDER)
|
|
117
|
+
dot = YELLOW if state == "run" else GREEN
|
|
118
|
+
d.ellipse([x0 + 12, y0 + 11, x0 + 20, y0 + 19], fill=dot)
|
|
119
|
+
d.text((x0 + 30, y0 + 6), label, font=FM, fill=GRAY)
|
|
120
|
+
tail = " running…" if state == "run" else " done"
|
|
121
|
+
d.text((x1 - 6, y0 + 6), "", font=FM)
|
|
122
|
+
d.text((x0 + 30 + tw + 6, y0 + 6), tail, font=FM, fill=dot)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def push(visible=USER_TEXT, pill=None, msg=False, reveal=0, nb=0, dur=90, blue_line=True):
|
|
126
|
+
img, d = base()
|
|
127
|
+
user_bubble(d, visible)
|
|
128
|
+
tool_pill(d, pill)
|
|
129
|
+
if msg:
|
|
130
|
+
d.text((PAD, msg_y), "Scanned 1 page — here's what's on it:", font=F, fill=WHITE)
|
|
131
|
+
if reveal > 0:
|
|
132
|
+
h = int(doc_h * min(reveal, 1.0))
|
|
133
|
+
img.paste(doc.crop((0, 0, doc_w, h)), (doc_x, doc_y))
|
|
134
|
+
if blue_line and reveal < 1.0:
|
|
135
|
+
ImageDraw.Draw(img).line(
|
|
136
|
+
[doc_x, doc_y + h, doc_x + doc_w, doc_y + h], fill=(88, 166, 255), width=2
|
|
137
|
+
)
|
|
138
|
+
if nb > 0:
|
|
139
|
+
d2 = ImageDraw.Draw(img)
|
|
140
|
+
y = bullets_y
|
|
141
|
+
for i in range(nb):
|
|
142
|
+
d2.text((tx, y), "●", font=FB, fill=GREEN)
|
|
143
|
+
d2.text((tx + 22, y), bullets[i], font=F, fill=WHITE)
|
|
144
|
+
y += LH + 8
|
|
145
|
+
frames.append(img)
|
|
146
|
+
durations.append(dur)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# 1) user types the request
|
|
150
|
+
for i in range(len(USER_TEXT) + 1):
|
|
151
|
+
push(visible=USER_TEXT[:i], dur=45)
|
|
152
|
+
push(dur=650)
|
|
153
|
+
|
|
154
|
+
# 2) Claude invokes the tool
|
|
155
|
+
push(pill="run", dur=500)
|
|
156
|
+
push(pill="run", dur=900)
|
|
157
|
+
push(pill="done", dur=600)
|
|
158
|
+
|
|
159
|
+
# 3) message + scan-line reveal of the page
|
|
160
|
+
push(pill="done", msg=True, dur=450)
|
|
161
|
+
reveal_steps = 14
|
|
162
|
+
for s in range(1, reveal_steps + 1):
|
|
163
|
+
push(pill="done", msg=True, reveal=s / reveal_steps, dur=70)
|
|
164
|
+
|
|
165
|
+
# 4) Claude's read-back bullets type in
|
|
166
|
+
for nb in range(len(bullets) + 1):
|
|
167
|
+
push(pill="done", msg=True, reveal=1.0, nb=nb, dur=90 if nb == 0 else 360)
|
|
168
|
+
push(pill="done", msg=True, reveal=1.0, nb=len(bullets), dur=2600)
|
|
169
|
+
|
|
170
|
+
# --- save -----------------------------------------------------------------
|
|
171
|
+
pal = [f.quantize(colors=128, method=Image.FASTOCTREE) for f in frames]
|
|
172
|
+
pal[0].save(
|
|
173
|
+
OUT, save_all=True, append_images=pal[1:], duration=durations,
|
|
174
|
+
loop=0, optimize=True, disposal=2,
|
|
175
|
+
)
|
|
176
|
+
print(f"wrote {OUT} ({len(frames)} frames, {OUT.stat().st_size/1024:.0f} KB)")
|