docalyze-mcp-server 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.
- docalyze_mcp_server-0.1.0/.gitignore +9 -0
- docalyze_mcp_server-0.1.0/LICENSE +21 -0
- docalyze_mcp_server-0.1.0/PKG-INFO +147 -0
- docalyze_mcp_server-0.1.0/README.md +115 -0
- docalyze_mcp_server-0.1.0/docalyze_mcp_server.py +628 -0
- docalyze_mcp_server-0.1.0/pyproject.toml +42 -0
- docalyze_mcp_server-0.1.0/server.json +20 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Juan Esteban Mosquera
|
|
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,147 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: docalyze-mcp-server
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for reading and visually analyzing local documents (PDF, Excel, CSV, Word, PowerPoint, images). No API keys required — works with GitHub Copilot and any MCP-compatible AI host.
|
|
5
|
+
Project-URL: Repository, https://github.com/LunarPerovskite/docalyze-mcp-server
|
|
6
|
+
Author: Juan Esteban Mosquera
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: copilot,docalyze,documents,excel,mcp,pdf,vscode
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Text Processing :: General
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Requires-Dist: mcp>=1.0.0
|
|
16
|
+
Requires-Dist: openpyxl>=3.1
|
|
17
|
+
Requires-Dist: pandas>=2.0
|
|
18
|
+
Requires-Dist: pdfplumber>=0.10
|
|
19
|
+
Provides-Extra: all
|
|
20
|
+
Requires-Dist: pillow>=10.0; extra == 'all'
|
|
21
|
+
Requires-Dist: pytesseract>=0.3; extra == 'all'
|
|
22
|
+
Requires-Dist: python-docx>=1.0; extra == 'all'
|
|
23
|
+
Requires-Dist: python-pptx>=0.6; extra == 'all'
|
|
24
|
+
Provides-Extra: docx
|
|
25
|
+
Requires-Dist: python-docx>=1.0; extra == 'docx'
|
|
26
|
+
Provides-Extra: ocr
|
|
27
|
+
Requires-Dist: pillow>=10.0; extra == 'ocr'
|
|
28
|
+
Requires-Dist: pytesseract>=0.3; extra == 'ocr'
|
|
29
|
+
Provides-Extra: pptx
|
|
30
|
+
Requires-Dist: python-pptx>=0.6; extra == 'pptx'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
<!-- mcp-name: io.github.LunarPerovskite/docalyze -->
|
|
34
|
+
|
|
35
|
+
# Docalyze MCP Server
|
|
36
|
+
|
|
37
|
+
An MCP (Model Context Protocol) server that lets AI assistants read and visually analyze local documents — PDFs, Excel spreadsheets, CSV files, Word documents, PowerPoint presentations, and images.
|
|
38
|
+
|
|
39
|
+
No API keys required. The host AI (GitHub Copilot, Claude, etc.) does all the reasoning directly.
|
|
40
|
+
|
|
41
|
+
## Supported Formats
|
|
42
|
+
|
|
43
|
+
| Format | Extensions | Read | Visual |
|
|
44
|
+
|--------|-----------|:----:|:------:|
|
|
45
|
+
| PDF | `.pdf` | ✅ | ✅ |
|
|
46
|
+
| Excel | `.xlsx`, `.xls` | ✅ | ✅ |
|
|
47
|
+
| CSV / TSV | `.csv`, `.tsv` | ✅ | — |
|
|
48
|
+
| JSON | `.json` | ✅ | — |
|
|
49
|
+
| Word | `.docx` | ✅ | ✅ |
|
|
50
|
+
| PowerPoint | `.pptx` | ✅ | ✅ |
|
|
51
|
+
| Plain text | `.txt`, `.md` | ✅ | — |
|
|
52
|
+
| Images | `.png`, `.jpg`, `.jpeg`, `.gif`, `.bmp`, `.tiff`, `.webp` | — | ✅ |
|
|
53
|
+
|
|
54
|
+
## Tools
|
|
55
|
+
|
|
56
|
+
| Tool | Description |
|
|
57
|
+
|------|-------------|
|
|
58
|
+
| `list_documents` | List files under a directory, filtered by glob pattern |
|
|
59
|
+
| `document_info` | Get metadata (size, modified date, sheets) for a file |
|
|
60
|
+
| `read_document` | Extract text content from a document with pagination |
|
|
61
|
+
| `visual_evaluate_document` | Return page images inline so the AI can analyze charts, tables, and diagrams |
|
|
62
|
+
|
|
63
|
+
## Installation
|
|
64
|
+
|
|
65
|
+
### From VS Code (recommended)
|
|
66
|
+
|
|
67
|
+
Search for **docalyze** in the MCP server gallery (Extensions sidebar → MCP tab) and click Install.
|
|
68
|
+
|
|
69
|
+
### From PyPI
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pip install docalyze-mcp-server
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Manual setup
|
|
76
|
+
|
|
77
|
+
Add to your VS Code `mcp.json` (or `settings.json`):
|
|
78
|
+
|
|
79
|
+
```jsonc
|
|
80
|
+
{
|
|
81
|
+
"servers": {
|
|
82
|
+
"docalyze": {
|
|
83
|
+
"type": "stdio",
|
|
84
|
+
"command": "python",
|
|
85
|
+
"args": ["-m", "docalyze_mcp_server"],
|
|
86
|
+
"env": {
|
|
87
|
+
"PYTHONIOENCODING": "utf-8"
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Or, if you installed via pip and want to use the entry point:
|
|
95
|
+
|
|
96
|
+
```jsonc
|
|
97
|
+
{
|
|
98
|
+
"servers": {
|
|
99
|
+
"docalyze": {
|
|
100
|
+
"type": "stdio",
|
|
101
|
+
"command": "docalyze-mcp-server"
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Optional Dependencies
|
|
108
|
+
|
|
109
|
+
The base install handles PDF, Excel, CSV, JSON, and plain text. For additional formats:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
# Word documents
|
|
113
|
+
pip install docalyze-mcp-server[docx]
|
|
114
|
+
|
|
115
|
+
# PowerPoint
|
|
116
|
+
pip install docalyze-mcp-server[pptx]
|
|
117
|
+
|
|
118
|
+
# OCR (requires Tesseract installed on your system)
|
|
119
|
+
pip install docalyze-mcp-server[ocr]
|
|
120
|
+
|
|
121
|
+
# Everything
|
|
122
|
+
pip install docalyze-mcp-server[all]
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Configuration
|
|
126
|
+
|
|
127
|
+
The server reads documents from a configurable root directory. Set the `DOCUMENTS_ROOT` environment variable to change it:
|
|
128
|
+
|
|
129
|
+
```jsonc
|
|
130
|
+
{
|
|
131
|
+
"servers": {
|
|
132
|
+
"docalyze": {
|
|
133
|
+
"type": "stdio",
|
|
134
|
+
"command": "docalyze-mcp-server",
|
|
135
|
+
"env": {
|
|
136
|
+
"DOCUMENTS_ROOT": "/path/to/your/documents"
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
If not set, it defaults to the directory containing the server script.
|
|
144
|
+
|
|
145
|
+
## License
|
|
146
|
+
|
|
147
|
+
MIT
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
<!-- mcp-name: io.github.LunarPerovskite/docalyze -->
|
|
2
|
+
|
|
3
|
+
# Docalyze MCP Server
|
|
4
|
+
|
|
5
|
+
An MCP (Model Context Protocol) server that lets AI assistants read and visually analyze local documents — PDFs, Excel spreadsheets, CSV files, Word documents, PowerPoint presentations, and images.
|
|
6
|
+
|
|
7
|
+
No API keys required. The host AI (GitHub Copilot, Claude, etc.) does all the reasoning directly.
|
|
8
|
+
|
|
9
|
+
## Supported Formats
|
|
10
|
+
|
|
11
|
+
| Format | Extensions | Read | Visual |
|
|
12
|
+
|--------|-----------|:----:|:------:|
|
|
13
|
+
| PDF | `.pdf` | ✅ | ✅ |
|
|
14
|
+
| Excel | `.xlsx`, `.xls` | ✅ | ✅ |
|
|
15
|
+
| CSV / TSV | `.csv`, `.tsv` | ✅ | — |
|
|
16
|
+
| JSON | `.json` | ✅ | — |
|
|
17
|
+
| Word | `.docx` | ✅ | ✅ |
|
|
18
|
+
| PowerPoint | `.pptx` | ✅ | ✅ |
|
|
19
|
+
| Plain text | `.txt`, `.md` | ✅ | — |
|
|
20
|
+
| Images | `.png`, `.jpg`, `.jpeg`, `.gif`, `.bmp`, `.tiff`, `.webp` | — | ✅ |
|
|
21
|
+
|
|
22
|
+
## Tools
|
|
23
|
+
|
|
24
|
+
| Tool | Description |
|
|
25
|
+
|------|-------------|
|
|
26
|
+
| `list_documents` | List files under a directory, filtered by glob pattern |
|
|
27
|
+
| `document_info` | Get metadata (size, modified date, sheets) for a file |
|
|
28
|
+
| `read_document` | Extract text content from a document with pagination |
|
|
29
|
+
| `visual_evaluate_document` | Return page images inline so the AI can analyze charts, tables, and diagrams |
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
### From VS Code (recommended)
|
|
34
|
+
|
|
35
|
+
Search for **docalyze** in the MCP server gallery (Extensions sidebar → MCP tab) and click Install.
|
|
36
|
+
|
|
37
|
+
### From PyPI
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install docalyze-mcp-server
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Manual setup
|
|
44
|
+
|
|
45
|
+
Add to your VS Code `mcp.json` (or `settings.json`):
|
|
46
|
+
|
|
47
|
+
```jsonc
|
|
48
|
+
{
|
|
49
|
+
"servers": {
|
|
50
|
+
"docalyze": {
|
|
51
|
+
"type": "stdio",
|
|
52
|
+
"command": "python",
|
|
53
|
+
"args": ["-m", "docalyze_mcp_server"],
|
|
54
|
+
"env": {
|
|
55
|
+
"PYTHONIOENCODING": "utf-8"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Or, if you installed via pip and want to use the entry point:
|
|
63
|
+
|
|
64
|
+
```jsonc
|
|
65
|
+
{
|
|
66
|
+
"servers": {
|
|
67
|
+
"docalyze": {
|
|
68
|
+
"type": "stdio",
|
|
69
|
+
"command": "docalyze-mcp-server"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Optional Dependencies
|
|
76
|
+
|
|
77
|
+
The base install handles PDF, Excel, CSV, JSON, and plain text. For additional formats:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
# Word documents
|
|
81
|
+
pip install docalyze-mcp-server[docx]
|
|
82
|
+
|
|
83
|
+
# PowerPoint
|
|
84
|
+
pip install docalyze-mcp-server[pptx]
|
|
85
|
+
|
|
86
|
+
# OCR (requires Tesseract installed on your system)
|
|
87
|
+
pip install docalyze-mcp-server[ocr]
|
|
88
|
+
|
|
89
|
+
# Everything
|
|
90
|
+
pip install docalyze-mcp-server[all]
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Configuration
|
|
94
|
+
|
|
95
|
+
The server reads documents from a configurable root directory. Set the `DOCUMENTS_ROOT` environment variable to change it:
|
|
96
|
+
|
|
97
|
+
```jsonc
|
|
98
|
+
{
|
|
99
|
+
"servers": {
|
|
100
|
+
"docalyze": {
|
|
101
|
+
"type": "stdio",
|
|
102
|
+
"command": "docalyze-mcp-server",
|
|
103
|
+
"env": {
|
|
104
|
+
"DOCUMENTS_ROOT": "/path/to/your/documents"
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
If not set, it defaults to the directory containing the server script.
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
MIT
|
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Docalyze MCP Server
|
|
3
|
+
|
|
4
|
+
Provides Model Context Protocol tools to inspect and read local documents
|
|
5
|
+
(PDF, Excel, CSV, JSON, TXT, Markdown). Designed to be launched via
|
|
6
|
+
VS Code's MCP configuration.
|
|
7
|
+
|
|
8
|
+
No external LLM API keys required — the host AI (e.g. GitHub Copilot)
|
|
9
|
+
performs all analysis on the returned content, including images.
|
|
10
|
+
"""
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import base64
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
import pandas as pd
|
|
18
|
+
import pdfplumber
|
|
19
|
+
from mcp.server.fastmcp import FastMCP
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
import openpyxl
|
|
23
|
+
from openpyxl.drawing.image import Image as XLImage
|
|
24
|
+
except ImportError: # pragma: no cover
|
|
25
|
+
openpyxl = None
|
|
26
|
+
XLImage = None
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
import docx # optional dependency
|
|
30
|
+
except ImportError: # pragma: no cover
|
|
31
|
+
docx = None
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
import pptx
|
|
35
|
+
except ImportError:
|
|
36
|
+
pptx = None
|
|
37
|
+
|
|
38
|
+
try: # optional OCR dependencies
|
|
39
|
+
import pytesseract
|
|
40
|
+
from PIL import Image
|
|
41
|
+
except ImportError: # pragma: no cover
|
|
42
|
+
pytesseract = None
|
|
43
|
+
Image = None
|
|
44
|
+
|
|
45
|
+
# ----------------------------------------------------------------------------
|
|
46
|
+
# Configuration
|
|
47
|
+
# ----------------------------------------------------------------------------
|
|
48
|
+
DEFAULT_ROOT = Path(os.getenv("DOCUMENTS_ROOT", Path(__file__).parent))
|
|
49
|
+
ALLOWED_EXTENSIONS = {
|
|
50
|
+
".pdf",
|
|
51
|
+
".txt",
|
|
52
|
+
".md",
|
|
53
|
+
".json",
|
|
54
|
+
".csv",
|
|
55
|
+
".xlsx",
|
|
56
|
+
".xls",
|
|
57
|
+
".tsv",
|
|
58
|
+
".docx",
|
|
59
|
+
".pptx",
|
|
60
|
+
".png",
|
|
61
|
+
".jpg",
|
|
62
|
+
".jpeg",
|
|
63
|
+
".bmp",
|
|
64
|
+
".tif",
|
|
65
|
+
".tiff",
|
|
66
|
+
}
|
|
67
|
+
IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".bmp", ".tif", ".tiff"}
|
|
68
|
+
|
|
69
|
+
server = FastMCP("docalyze")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _resolve_path(file_path: str) -> Path:
|
|
73
|
+
path = (DEFAULT_ROOT / file_path).resolve() if not Path(file_path).is_absolute() else Path(file_path).resolve()
|
|
74
|
+
if not str(path).startswith(str(DEFAULT_ROOT.resolve())):
|
|
75
|
+
raise ValueError("Access outside of root directory is not allowed")
|
|
76
|
+
if not path.exists():
|
|
77
|
+
raise FileNotFoundError(f"File not found: {path}")
|
|
78
|
+
if path.suffix.lower() not in ALLOWED_EXTENSIONS:
|
|
79
|
+
raise ValueError(f"Unsupported file type: {path.suffix}")
|
|
80
|
+
return path
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _read_text_file(path: Path, max_chars: int) -> str:
|
|
84
|
+
data = path.read_text(encoding="utf-8", errors="ignore")
|
|
85
|
+
return data[:max_chars]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _read_json_file(path: Path, max_chars: int) -> str:
|
|
89
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
90
|
+
pretty = json.dumps(data, indent=2)
|
|
91
|
+
return pretty[:max_chars]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _read_csv_file(path: Path, max_rows: int, sep: str = ",") -> str:
|
|
95
|
+
df = pd.read_csv(path, sep=sep, nrows=max_rows)
|
|
96
|
+
return df.to_string(index=False)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _read_excel_file(path: Path, sheet: Optional[str], start_row: int, num_rows: int) -> str:
|
|
100
|
+
"""Read Excel file with full openpyxl support for chart/image detection."""
|
|
101
|
+
try:
|
|
102
|
+
from openpyxl import load_workbook
|
|
103
|
+
except ImportError:
|
|
104
|
+
raise ImportError("openpyxl is required for Excel files; install it with: pip install openpyxl")
|
|
105
|
+
|
|
106
|
+
# Load workbook to inspect structure
|
|
107
|
+
wb = load_workbook(path, data_only=True)
|
|
108
|
+
available_sheets = wb.sheetnames
|
|
109
|
+
|
|
110
|
+
# Validate requested sheet
|
|
111
|
+
if sheet:
|
|
112
|
+
if sheet not in available_sheets:
|
|
113
|
+
wb.close()
|
|
114
|
+
raise ValueError(
|
|
115
|
+
f"Sheet '{sheet}' not found. Available sheets: {', '.join(available_sheets)}"
|
|
116
|
+
)
|
|
117
|
+
sheet_name = sheet
|
|
118
|
+
else:
|
|
119
|
+
sheet_name = available_sheets[0]
|
|
120
|
+
|
|
121
|
+
ws = wb[sheet_name]
|
|
122
|
+
|
|
123
|
+
# Detect charts and images (filter small logos)
|
|
124
|
+
has_charts = len(ws._charts) > 0 if hasattr(ws, '_charts') else False
|
|
125
|
+
|
|
126
|
+
# Count only significant images (>= 50x50 pixels)
|
|
127
|
+
significant_image_count = 0
|
|
128
|
+
if hasattr(ws, '_images'):
|
|
129
|
+
for img in ws._images:
|
|
130
|
+
# Check image dimensions
|
|
131
|
+
if hasattr(img, 'width') and hasattr(img, 'height'):
|
|
132
|
+
# openpyxl stores dimensions in EMUs (English Metric Units)
|
|
133
|
+
# 914400 EMUs = 1 inch, typical 96 DPI means ~9525 EMUs per pixel
|
|
134
|
+
width_pixels = img.width / 9525 if img.width else 0
|
|
135
|
+
height_pixels = img.height / 9525 if img.height else 0
|
|
136
|
+
# Only count images >= 200x200 pixels (filter logos)
|
|
137
|
+
if width_pixels >= 200 and height_pixels >= 200:
|
|
138
|
+
significant_image_count += 1
|
|
139
|
+
else:
|
|
140
|
+
# If no dimensions, count it
|
|
141
|
+
significant_image_count += 1
|
|
142
|
+
|
|
143
|
+
has_images = significant_image_count > 0
|
|
144
|
+
chart_count = len(ws._charts) if has_charts else 0
|
|
145
|
+
image_count = significant_image_count
|
|
146
|
+
|
|
147
|
+
# Try to read data
|
|
148
|
+
try:
|
|
149
|
+
df = pd.read_excel(path, sheet_name=sheet_name)
|
|
150
|
+
if df.empty or df.dropna(how='all').empty:
|
|
151
|
+
# Empty data sheet, likely chart-only
|
|
152
|
+
wb.close()
|
|
153
|
+
sheets_list = ', '.join(f"'{s}'" for s in available_sheets)
|
|
154
|
+
header = f"[Available sheets: {sheets_list}]\nReading sheet: '{sheet_name}'\n\n"
|
|
155
|
+
msg = (
|
|
156
|
+
f"⚠️ VISUAL CONTENT DETECTED: Sheet contains {chart_count} chart(s) and {image_count} image(s) but no data table ⚠️\n\n"
|
|
157
|
+
f"RECOMMENDATION: Convert Excel to PDF first, then use:\n"
|
|
158
|
+
f" visual_evaluate_document(file_path='<pdf_path>', enable_vision=true, sheet='{sheet_name}')\n"
|
|
159
|
+
"to extract chart content with GPT-4 Vision.\n"
|
|
160
|
+
)
|
|
161
|
+
return header + msg
|
|
162
|
+
|
|
163
|
+
subset = df.iloc[start_row : start_row + num_rows]
|
|
164
|
+
wb.close()
|
|
165
|
+
|
|
166
|
+
# Include metadata in output
|
|
167
|
+
sheets_list = ', '.join(f"'{s}'" for s in available_sheets)
|
|
168
|
+
header = f"[Available sheets: {sheets_list}]\nReading sheet: '{sheet_name}'\n"
|
|
169
|
+
if has_charts or has_images:
|
|
170
|
+
header += (
|
|
171
|
+
f"\n⚠️ VISUAL CONTENT DETECTED: {chart_count} chart(s) and {image_count} image(s) in this sheet ⚠️\n"
|
|
172
|
+
f"NOTE: Text/data extraction successful (see below), but charts/images not extracted.\n"
|
|
173
|
+
f"RECOMMENDATION: To extract chart content, convert to PDF and use:\n"
|
|
174
|
+
f" visual_evaluate_document(file_path='<pdf_path>', enable_vision=true)\n"
|
|
175
|
+
)
|
|
176
|
+
header += "\n"
|
|
177
|
+
return header + subset.to_string(index=False)
|
|
178
|
+
|
|
179
|
+
except Exception as e:
|
|
180
|
+
# Sheet might be chart-only or corrupted
|
|
181
|
+
wb.close()
|
|
182
|
+
sheets_list = ', '.join(f"'{s}'" for s in available_sheets)
|
|
183
|
+
header = f"[Available sheets: {sheets_list}]\nReading sheet: '{sheet_name}'\n\n"
|
|
184
|
+
msg = (
|
|
185
|
+
f"⚠️ VISUAL CONTENT DETECTED: {chart_count} chart(s) and {image_count} image(s) ⚠️\n"
|
|
186
|
+
f"[Cannot read sheet as data table: {str(e)}]\n\n"
|
|
187
|
+
f"RECOMMENDATION: Convert Excel to PDF first, then use:\n"
|
|
188
|
+
f" visual_evaluate_document(file_path='<pdf_path>', enable_vision=true)\n"
|
|
189
|
+
"to analyze this chart-only sheet with GPT-4 Vision.\n"
|
|
190
|
+
)
|
|
191
|
+
return header + msg
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _read_pdf_file(path: Path, page: int, max_pages: int, max_chars: int) -> str:
|
|
195
|
+
"""Read PDF with detection of images/charts to recommend vision analysis."""
|
|
196
|
+
texts: List[str] = []
|
|
197
|
+
visual_content_detected = []
|
|
198
|
+
|
|
199
|
+
with pdfplumber.open(path) as pdf:
|
|
200
|
+
total_pages = len(pdf.pages)
|
|
201
|
+
start = max(page - 1, 0)
|
|
202
|
+
end = min(start + max_pages, total_pages)
|
|
203
|
+
|
|
204
|
+
for idx in range(start, end):
|
|
205
|
+
page_obj = pdf.pages[idx]
|
|
206
|
+
|
|
207
|
+
# Detect visual content on this page
|
|
208
|
+
# Filter out small images (logos, icons) - only count images >= 200x200 pixels
|
|
209
|
+
significant_images = []
|
|
210
|
+
if hasattr(page_obj, 'images'):
|
|
211
|
+
for img in page_obj.images:
|
|
212
|
+
# Check image dimensions
|
|
213
|
+
width = img.get('width', 0)
|
|
214
|
+
height = img.get('height', 0)
|
|
215
|
+
# Only count images that are large enough to contain data (>= 200x200)
|
|
216
|
+
# Logos and small icons are typically < 200 pixels
|
|
217
|
+
if width >= 200 and height >= 200:
|
|
218
|
+
significant_images.append(img)
|
|
219
|
+
|
|
220
|
+
has_images = len(significant_images) > 0
|
|
221
|
+
has_curves = len(page_obj.curves) > 0 if hasattr(page_obj, 'curves') else False
|
|
222
|
+
|
|
223
|
+
# Extract text
|
|
224
|
+
extracted = page_obj.extract_text() or ""
|
|
225
|
+
|
|
226
|
+
# Check if page is mostly images (scanned or has charts)
|
|
227
|
+
if has_images or (has_curves and len(extracted.strip()) < 100):
|
|
228
|
+
image_count = len(significant_images)
|
|
229
|
+
visual_content_detected.append(f"Page {idx + 1}: {image_count} image(s), possible chart/diagram")
|
|
230
|
+
|
|
231
|
+
header = f"\n--- Page {idx + 1}/{total_pages} ---\n"
|
|
232
|
+
texts.append(header + extracted)
|
|
233
|
+
|
|
234
|
+
joined = "\n".join(texts)
|
|
235
|
+
|
|
236
|
+
# Add recommendation if visual content detected
|
|
237
|
+
if visual_content_detected:
|
|
238
|
+
recommendation = (
|
|
239
|
+
"\n\n⚠️ VISUAL CONTENT DETECTED ⚠️\n"
|
|
240
|
+
+ "\n".join(visual_content_detected)
|
|
241
|
+
+ f"\n\nRECOMMENDATION: Use visual_evaluate_document(file_path='{path.name}', enable_vision=true, page={page}) "
|
|
242
|
+
"to extract charts, diagrams, and image content with GPT-4 Vision.\n"
|
|
243
|
+
)
|
|
244
|
+
joined = recommendation + joined
|
|
245
|
+
|
|
246
|
+
return joined[:max_chars + 500] # Allow extra space for recommendation
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _format_table(rows: List[List[str]]) -> str:
|
|
250
|
+
if not rows:
|
|
251
|
+
return ""
|
|
252
|
+
num_cols = max(len(row) for row in rows)
|
|
253
|
+
widths = [0] * num_cols
|
|
254
|
+
for row in rows:
|
|
255
|
+
for idx in range(num_cols):
|
|
256
|
+
cell = row[idx] if idx < len(row) else ""
|
|
257
|
+
widths[idx] = max(widths[idx], len(cell))
|
|
258
|
+
lines: List[str] = []
|
|
259
|
+
for row in rows:
|
|
260
|
+
cells: List[str] = []
|
|
261
|
+
for idx in range(num_cols):
|
|
262
|
+
cell = row[idx] if idx < len(row) else ""
|
|
263
|
+
cells.append(cell.ljust(widths[idx]))
|
|
264
|
+
lines.append(" | ".join(cells).rstrip())
|
|
265
|
+
return "\n".join(lines)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _read_docx_file(path: Path, max_chars: int, include_tables: bool) -> str:
|
|
269
|
+
"""Read Word document with detection of images and embedded objects."""
|
|
270
|
+
if docx is None:
|
|
271
|
+
raise ImportError("python-docx is required to read DOCX files")
|
|
272
|
+
|
|
273
|
+
document = docx.Document(str(path))
|
|
274
|
+
|
|
275
|
+
# Detect images and embedded objects (filter out small logos)
|
|
276
|
+
from PIL import Image as PILImage
|
|
277
|
+
import io
|
|
278
|
+
|
|
279
|
+
image_count = 0
|
|
280
|
+
for rel in document.part.rels.values():
|
|
281
|
+
if "image" in rel.target_ref:
|
|
282
|
+
try:
|
|
283
|
+
# Try to get image dimensions
|
|
284
|
+
image_part = rel.target_part
|
|
285
|
+
img_stream = io.BytesIO(image_part.blob)
|
|
286
|
+
img = PILImage.open(img_stream)
|
|
287
|
+
width, height = img.size
|
|
288
|
+
# Only count images >= 200x200 pixels (filter logos and small icons)
|
|
289
|
+
if width >= 200 and height >= 200:
|
|
290
|
+
image_count += 1
|
|
291
|
+
except:
|
|
292
|
+
# If we can't read dimensions, count it anyway
|
|
293
|
+
image_count += 1
|
|
294
|
+
|
|
295
|
+
# Count tables
|
|
296
|
+
table_count = len(document.tables)
|
|
297
|
+
|
|
298
|
+
# Extract text
|
|
299
|
+
paragraphs = [p.text for p in document.paragraphs]
|
|
300
|
+
text = "\n".join(paragraphs)
|
|
301
|
+
|
|
302
|
+
# Add metadata header with visual content detection
|
|
303
|
+
header_parts = []
|
|
304
|
+
if image_count > 0:
|
|
305
|
+
header_parts.append(f"{image_count} image(s)")
|
|
306
|
+
if table_count > 0:
|
|
307
|
+
header_parts.append(f"{table_count} table(s)")
|
|
308
|
+
|
|
309
|
+
if header_parts:
|
|
310
|
+
header = f"[Document contains: {', '.join(header_parts)}]\n"
|
|
311
|
+
if image_count > 0:
|
|
312
|
+
header += (
|
|
313
|
+
f"\n⚠️ VISUAL CONTENT DETECTED: {image_count} image(s) in this Word document ⚠️\n"
|
|
314
|
+
f"RECOMMENDATION: Convert to PDF first, then use:\n"
|
|
315
|
+
f" visual_evaluate_document(file_path='<pdf_path>', enable_vision=true)\n"
|
|
316
|
+
"to extract content from images with GPT-4 Vision.\n"
|
|
317
|
+
)
|
|
318
|
+
header += "\n"
|
|
319
|
+
text = header + text
|
|
320
|
+
|
|
321
|
+
# Extract tables if requested
|
|
322
|
+
if include_tables and document.tables:
|
|
323
|
+
table_chunks: List[str] = []
|
|
324
|
+
for idx, table in enumerate(document.tables, start=1):
|
|
325
|
+
tables_rows: List[List[str]] = []
|
|
326
|
+
for row in table.rows:
|
|
327
|
+
tables_rows.append([cell.text.strip() for cell in row.cells])
|
|
328
|
+
formatted = _format_table(tables_rows)
|
|
329
|
+
table_chunks.append(f"Table {idx}:\n{formatted}")
|
|
330
|
+
if table_chunks:
|
|
331
|
+
text += "\n\n" + "\n\n".join(table_chunks)
|
|
332
|
+
|
|
333
|
+
return text[:max_chars]
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _read_pptx_file(path: Path, max_chars: int) -> str:
|
|
337
|
+
"""Read PowerPoint file with detection of visuals."""
|
|
338
|
+
if pptx is None:
|
|
339
|
+
raise ImportError("python-pptx is required to read PPTX files")
|
|
340
|
+
|
|
341
|
+
prs = pptx.Presentation(str(path))
|
|
342
|
+
texts = []
|
|
343
|
+
|
|
344
|
+
# Metadata
|
|
345
|
+
slide_count = len(prs.slides)
|
|
346
|
+
header = f"[PowerPoint Presentation: {slide_count} slides]\n"
|
|
347
|
+
|
|
348
|
+
for i, slide in enumerate(prs.slides):
|
|
349
|
+
slide_text = []
|
|
350
|
+
title = slide.shapes.title.text if slide.shapes.title else "No Title"
|
|
351
|
+
slide_text.append(f"--- Slide {i+1}: {title} ---")
|
|
352
|
+
|
|
353
|
+
# Shapes text
|
|
354
|
+
for shape in slide.shapes:
|
|
355
|
+
if hasattr(shape, "text") and shape.text:
|
|
356
|
+
text_clean = shape.text.strip()
|
|
357
|
+
if text_clean and text_clean != title:
|
|
358
|
+
slide_text.append(text_clean)
|
|
359
|
+
|
|
360
|
+
if shape.has_table:
|
|
361
|
+
table = shape.table
|
|
362
|
+
rows_data = []
|
|
363
|
+
for row in table.rows:
|
|
364
|
+
rows_data.append([cell.text_frame.text.strip() for cell in row.cells])
|
|
365
|
+
formatted = _format_table(rows_data)
|
|
366
|
+
slide_text.append(f"\n[Table]\n{formatted}\n")
|
|
367
|
+
|
|
368
|
+
texts.append("\n".join(slide_text))
|
|
369
|
+
|
|
370
|
+
return header + "\n\n".join(texts)[:max_chars]
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _require_ocr_dependencies() -> None:
|
|
374
|
+
if pytesseract is None or Image is None:
|
|
375
|
+
raise ImportError("pytesseract and pillow are required for OCR; install them to enable this feature")
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _ocr_image(path: Path, lang: str) -> str:
|
|
380
|
+
_require_ocr_dependencies()
|
|
381
|
+
with Image.open(path) as img:
|
|
382
|
+
return pytesseract.image_to_string(img, lang=lang)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _ocr_pdf_pages(path: Path, page: int, max_pages: int, lang: str) -> str:
|
|
386
|
+
_require_ocr_dependencies()
|
|
387
|
+
texts: List[str] = []
|
|
388
|
+
with pdfplumber.open(path) as pdf:
|
|
389
|
+
total_pages = len(pdf.pages)
|
|
390
|
+
start = max(page - 1, 0)
|
|
391
|
+
end = min(start + max_pages, total_pages)
|
|
392
|
+
for idx in range(start, end):
|
|
393
|
+
page_obj = pdf.pages[idx]
|
|
394
|
+
page_image = page_obj.to_image(resolution=200).original
|
|
395
|
+
ocr_text = pytesseract.image_to_string(page_image, lang=lang)
|
|
396
|
+
texts.append(f"--- OCR Page {idx + 1}/{total_pages} ---\n{ocr_text.strip()}")
|
|
397
|
+
return "\n\n".join(texts)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _encode_image_base64(image_path: Path) -> str:
|
|
401
|
+
"""Encode image file to base64 string."""
|
|
402
|
+
with open(image_path, "rb") as img_file:
|
|
403
|
+
return base64.b64encode(img_file.read()).decode('utf-8')
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _pdf_page_to_base64(pdf_path: Path, page_num: int) -> str:
|
|
407
|
+
"""Convert a PDF page to base64-encoded image."""
|
|
408
|
+
_require_ocr_dependencies()
|
|
409
|
+
with pdfplumber.open(pdf_path) as pdf:
|
|
410
|
+
if page_num < 1 or page_num > len(pdf.pages):
|
|
411
|
+
raise ValueError(f"Page {page_num} out of range (1-{len(pdf.pages)})")
|
|
412
|
+
page = pdf.pages[page_num - 1]
|
|
413
|
+
page_image = page.to_image(resolution=200).original
|
|
414
|
+
from io import BytesIO
|
|
415
|
+
buffer = BytesIO()
|
|
416
|
+
page_image.save(buffer, format="PNG")
|
|
417
|
+
return base64.b64encode(buffer.getvalue()).decode('utf-8')
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _get_image_mime(path: Path) -> str:
|
|
421
|
+
"""Return MIME type for an image file."""
|
|
422
|
+
ext = path.suffix.lower()
|
|
423
|
+
return {
|
|
424
|
+
".png": "image/png",
|
|
425
|
+
".jpg": "image/jpeg",
|
|
426
|
+
".jpeg": "image/jpeg",
|
|
427
|
+
".bmp": "image/bmp",
|
|
428
|
+
".tif": "image/tiff",
|
|
429
|
+
".tiff": "image/tiff",
|
|
430
|
+
}.get(ext, "image/png")
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
@server.tool()
|
|
434
|
+
def list_documents(
|
|
435
|
+
root: Optional[str] = None,
|
|
436
|
+
pattern: str = "**/*",
|
|
437
|
+
limit: int = 100,
|
|
438
|
+
) -> List[Dict[str, Any]]:
|
|
439
|
+
"""List documents under the configured root directory."""
|
|
440
|
+
base = Path(root).resolve() if root else DEFAULT_ROOT.resolve()
|
|
441
|
+
results: List[Dict[str, Any]] = []
|
|
442
|
+
for path in base.glob(pattern):
|
|
443
|
+
if path.is_file() and path.suffix.lower() in ALLOWED_EXTENSIONS:
|
|
444
|
+
results.append({
|
|
445
|
+
"path": str(path),
|
|
446
|
+
"size_bytes": path.stat().st_size,
|
|
447
|
+
"suffix": path.suffix,
|
|
448
|
+
})
|
|
449
|
+
if len(results) >= limit:
|
|
450
|
+
break
|
|
451
|
+
return results
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
@server.tool()
|
|
455
|
+
def document_info(file_path: str) -> Dict[str, Any]:
|
|
456
|
+
"""Return metadata about a specific document."""
|
|
457
|
+
path = _resolve_path(file_path)
|
|
458
|
+
info = path.stat()
|
|
459
|
+
response: Dict[str, Any] = {
|
|
460
|
+
"path": str(path),
|
|
461
|
+
"size_bytes": info.st_size,
|
|
462
|
+
"modified": info.st_mtime,
|
|
463
|
+
"extension": path.suffix,
|
|
464
|
+
}
|
|
465
|
+
if path.suffix.lower() in {".xlsx", ".xls"}:
|
|
466
|
+
try:
|
|
467
|
+
sheets = pd.ExcelFile(path).sheet_names
|
|
468
|
+
except Exception as exc: # pragma: no cover
|
|
469
|
+
sheets = [f"Error: {exc}"]
|
|
470
|
+
response["sheets"] = sheets
|
|
471
|
+
return response
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
@server.tool()
|
|
475
|
+
def read_document(
|
|
476
|
+
file_path: str,
|
|
477
|
+
max_chars: int = 8000,
|
|
478
|
+
page: int = 1,
|
|
479
|
+
max_pages: int = 2,
|
|
480
|
+
sheet: Optional[str] = None,
|
|
481
|
+
start_row: int = 0,
|
|
482
|
+
num_rows: int = 50,
|
|
483
|
+
include_tables: bool = False,
|
|
484
|
+
) -> str:
|
|
485
|
+
"""Read a portion of a document (PDF, Excel, CSV, JSON, TXT, DOCX).
|
|
486
|
+
|
|
487
|
+
IMPORTANT: When presenting results to users, ALWAYS mention:
|
|
488
|
+
- The folder/directory path (e.g., 'Well 1/Well Test/')
|
|
489
|
+
- The complete document name
|
|
490
|
+
This helps users understand which well and document type is being referenced.
|
|
491
|
+
"""
|
|
492
|
+
path = _resolve_path(file_path)
|
|
493
|
+
suffix = path.suffix.lower()
|
|
494
|
+
|
|
495
|
+
# Add context header with folder and filename
|
|
496
|
+
folder_path = str(path.parent.relative_to(DEFAULT_ROOT)) if path.is_relative_to(DEFAULT_ROOT) else str(path.parent)
|
|
497
|
+
context_header = f"📁 Document: {folder_path}/{path.name}\n" + "="*80 + "\n\n"
|
|
498
|
+
|
|
499
|
+
if suffix in {".txt", ".md"}:
|
|
500
|
+
return context_header + _read_text_file(path, max_chars)
|
|
501
|
+
if suffix == ".json":
|
|
502
|
+
return context_header + _read_json_file(path, max_chars)
|
|
503
|
+
if suffix in {".csv", ".tsv"}:
|
|
504
|
+
sep = "\t" if suffix == ".tsv" else ","
|
|
505
|
+
return context_header + _read_csv_file(path, num_rows, sep)
|
|
506
|
+
if suffix in {".xlsx", ".xls"}:
|
|
507
|
+
return context_header + _read_excel_file(path, sheet, start_row, num_rows)
|
|
508
|
+
if suffix == ".pdf":
|
|
509
|
+
return context_header + _read_pdf_file(path, page, max_pages, max_chars)
|
|
510
|
+
if suffix == ".docx":
|
|
511
|
+
return context_header + _read_docx_file(path, max_chars, include_tables)
|
|
512
|
+
if suffix == ".pptx":
|
|
513
|
+
return context_header + _read_pptx_file(path, max_chars)
|
|
514
|
+
|
|
515
|
+
raise ValueError(f"Unsupported file type: {suffix}")
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
@server.tool()
|
|
519
|
+
def visual_evaluate_document(
|
|
520
|
+
file_path: str,
|
|
521
|
+
enable_ocr: bool = False,
|
|
522
|
+
page: int = 1,
|
|
523
|
+
max_pages: int = 1,
|
|
524
|
+
ocr_lang: str = "eng",
|
|
525
|
+
) -> list:
|
|
526
|
+
"""Extract visual content from a document (PDF pages or images) and return
|
|
527
|
+
it as inline images so the host AI can analyse charts, tables, and diagrams
|
|
528
|
+
directly. Optionally run local Tesseract OCR as well.
|
|
529
|
+
|
|
530
|
+
Returns a list of content blocks (text and/or base64 images) that the
|
|
531
|
+
calling AI model can interpret.
|
|
532
|
+
"""
|
|
533
|
+
from mcp.types import TextContent, ImageContent
|
|
534
|
+
|
|
535
|
+
path = _resolve_path(file_path)
|
|
536
|
+
suffix = path.suffix.lower()
|
|
537
|
+
|
|
538
|
+
folder_path = (
|
|
539
|
+
str(path.parent.relative_to(DEFAULT_ROOT))
|
|
540
|
+
if path.is_relative_to(DEFAULT_ROOT)
|
|
541
|
+
else str(path.parent)
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
contents: list = []
|
|
545
|
+
contents.append(
|
|
546
|
+
TextContent(
|
|
547
|
+
type="text",
|
|
548
|
+
text=f"Document: {folder_path}/{path.name}",
|
|
549
|
+
)
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
# ---- Return images so the host AI can see them ----
|
|
553
|
+
if suffix in IMAGE_EXTENSIONS:
|
|
554
|
+
mime = _get_image_mime(path)
|
|
555
|
+
b64 = _encode_image_base64(path)
|
|
556
|
+
contents.append(
|
|
557
|
+
ImageContent(type="image", data=b64, mimeType=mime)
|
|
558
|
+
)
|
|
559
|
+
elif suffix == ".pdf":
|
|
560
|
+
try:
|
|
561
|
+
with pdfplumber.open(path) as pdf:
|
|
562
|
+
total = len(pdf.pages)
|
|
563
|
+
start = max(page - 1, 0)
|
|
564
|
+
end = min(start + max_pages, total)
|
|
565
|
+
for idx in range(start, end):
|
|
566
|
+
page_b64 = _pdf_page_to_base64(path, idx + 1)
|
|
567
|
+
contents.append(
|
|
568
|
+
TextContent(type="text", text=f"--- Page {idx + 1}/{total} ---")
|
|
569
|
+
)
|
|
570
|
+
contents.append(
|
|
571
|
+
ImageContent(type="image", data=page_b64, mimeType="image/png")
|
|
572
|
+
)
|
|
573
|
+
except Exception as exc:
|
|
574
|
+
contents.append(
|
|
575
|
+
TextContent(type="text", text=f"Failed to render PDF pages: {exc}")
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
# Also include pdfplumber extracted text/tables as supplementary data
|
|
579
|
+
try:
|
|
580
|
+
with pdfplumber.open(path) as pdf:
|
|
581
|
+
total = len(pdf.pages)
|
|
582
|
+
start = max(page - 1, 0)
|
|
583
|
+
end = min(start + max_pages, total)
|
|
584
|
+
for idx in range(start, end):
|
|
585
|
+
pg = pdf.pages[idx]
|
|
586
|
+
text = pg.extract_text() or ""
|
|
587
|
+
tables = pg.extract_tables()
|
|
588
|
+
parts = [f"Page {idx + 1} extracted text:\n{text}"]
|
|
589
|
+
for ti, table in enumerate(tables, 1):
|
|
590
|
+
rows = "\n".join(
|
|
591
|
+
" | ".join(str(c or "") for c in row) for row in table
|
|
592
|
+
)
|
|
593
|
+
parts.append(f"Table {ti}:\n{rows}")
|
|
594
|
+
contents.append(
|
|
595
|
+
TextContent(type="text", text="\n\n".join(parts))
|
|
596
|
+
)
|
|
597
|
+
except Exception:
|
|
598
|
+
pass
|
|
599
|
+
else:
|
|
600
|
+
contents.append(
|
|
601
|
+
TextContent(
|
|
602
|
+
type="text",
|
|
603
|
+
text=f"Visual extraction not supported for {suffix}; use read_document instead.",
|
|
604
|
+
)
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
# ---- Optional local OCR ----
|
|
608
|
+
if enable_ocr:
|
|
609
|
+
try:
|
|
610
|
+
if suffix == ".pdf":
|
|
611
|
+
ocr_text = _ocr_pdf_pages(path, page, max_pages, ocr_lang)
|
|
612
|
+
elif suffix in IMAGE_EXTENSIONS:
|
|
613
|
+
ocr_text = _ocr_image(path, ocr_lang)
|
|
614
|
+
else:
|
|
615
|
+
ocr_text = f"OCR not supported for {suffix}"
|
|
616
|
+
contents.append(
|
|
617
|
+
TextContent(type="text", text=f"OCR result:\n{ocr_text}")
|
|
618
|
+
)
|
|
619
|
+
except Exception as exc:
|
|
620
|
+
contents.append(
|
|
621
|
+
TextContent(type="text", text=f"OCR failed: {exc}")
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
return contents
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
if __name__ == "__main__":
|
|
628
|
+
server.run()
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "docalyze-mcp-server"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "MCP server for reading and visually analyzing local documents (PDF, Excel, CSV, Word, PowerPoint, images). No API keys required — works with GitHub Copilot and any MCP-compatible AI host."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [{ name = "Juan Esteban Mosquera" }]
|
|
13
|
+
keywords = ["mcp", "docalyze", "documents", "pdf", "excel", "copilot", "vscode"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Topic :: Text Processing :: General",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"mcp>=1.0.0",
|
|
22
|
+
"pandas>=2.0",
|
|
23
|
+
"pdfplumber>=0.10",
|
|
24
|
+
"openpyxl>=3.1",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
docx = ["python-docx>=1.0"]
|
|
29
|
+
pptx = ["python-pptx>=0.6"]
|
|
30
|
+
ocr = ["pytesseract>=0.3", "Pillow>=10.0"]
|
|
31
|
+
all = [
|
|
32
|
+
"python-docx>=1.0",
|
|
33
|
+
"python-pptx>=0.6",
|
|
34
|
+
"pytesseract>=0.3",
|
|
35
|
+
"Pillow>=10.0",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.scripts]
|
|
39
|
+
docalyze-mcp-server = "docalyze_mcp_server:server.run"
|
|
40
|
+
|
|
41
|
+
[project.urls]
|
|
42
|
+
Repository = "https://github.com/LunarPerovskite/docalyze-mcp-server"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.LunarPerovskite/docalyze",
|
|
4
|
+
"description": "Read and analyze local documents (PDF, Excel, CSV, Word, PowerPoint, images). No API keys needed.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"url": "https://github.com/LunarPerovskite/docalyze-mcp-server",
|
|
7
|
+
"source": "github"
|
|
8
|
+
},
|
|
9
|
+
"version": "0.1.0",
|
|
10
|
+
"packages": [
|
|
11
|
+
{
|
|
12
|
+
"registryType": "pypi",
|
|
13
|
+
"identifier": "docalyze-mcp-server",
|
|
14
|
+
"version": "0.1.0",
|
|
15
|
+
"transport": {
|
|
16
|
+
"type": "stdio"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|