Rename package to mcwaddams
Some checks are pending
Test Dashboard / test-and-dashboard (push) Waiting to run
Some checks are pending
Test Dashboard / test-and-dashboard (push) Waiting to run
Named for Milton Waddams, who was relocated to the basement with boxes of legacy documents. He handles the .doc and .xls files from 1997 that nobody else wants to touch. - Rename package from mcp-office-tools to mcwaddams - Update author to Ryan Malloy - Update all imports and references - Add Office Space themed README narrative - All 53 tests passing
This commit is contained in:
parent
6fb76d8760
commit
31948d6ffc
20
CLAUDE.md
20
CLAUDE.md
@ -1,10 +1,10 @@
|
|||||||
# CLAUDE.md
|
# CLAUDE.md
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with the MCP Office Tools codebase.
|
This file provides guidance to Claude Code (claude.ai/code) when working with the mcwaddams codebase.
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
MCP Office Tools is a FastMCP server that provides comprehensive Microsoft Office document processing capabilities including text extraction, image extraction, metadata extraction, and format detection. The server supports Word (.docx, .doc), Excel (.xlsx, .xls), PowerPoint (.pptx, .ppt), and CSV files with intelligent method selection and automatic fallbacks.
|
mcwaddams is a FastMCP server that provides comprehensive Microsoft Office document processing capabilities including text extraction, image extraction, metadata extraction, and format detection. The server supports Word (.docx, .doc), Excel (.xlsx, .xls), PowerPoint (.pptx, .ppt), and CSV files with intelligent method selection and automatic fallbacks.
|
||||||
|
|
||||||
## Development Commands
|
## Development Commands
|
||||||
|
|
||||||
@ -23,7 +23,7 @@ uv sync --dev
|
|||||||
uv run pytest
|
uv run pytest
|
||||||
|
|
||||||
# Run with coverage
|
# Run with coverage
|
||||||
uv run pytest --cov=mcp_office_tools
|
uv run pytest --cov=mcwaddams
|
||||||
|
|
||||||
# Run specific test file
|
# Run specific test file
|
||||||
uv run pytest tests/test_server.py
|
uv run pytest tests/test_server.py
|
||||||
@ -47,10 +47,10 @@ uv run mypy src/
|
|||||||
### Running the Server
|
### Running the Server
|
||||||
```bash
|
```bash
|
||||||
# Run MCP server directly
|
# Run MCP server directly
|
||||||
uv run mcp-office-tools
|
uv run mcwaddams
|
||||||
|
|
||||||
# Run with Python module
|
# Run with Python module
|
||||||
uv run python -m mcp_office_tools.server
|
uv run python -m mcwaddams.server
|
||||||
|
|
||||||
# Test with sample documents
|
# Test with sample documents
|
||||||
uv run python examples/test_office_tools.py /path/to/test.docx
|
uv run python examples/test_office_tools.py /path/to/test.docx
|
||||||
@ -69,8 +69,8 @@ uv publish
|
|||||||
|
|
||||||
### Core Components
|
### Core Components
|
||||||
|
|
||||||
- **`src/mcp_office_tools/server.py`**: Main server implementation with all Office processing tools
|
- **`src/mcwaddams/server.py`**: Main server implementation with all Office processing tools
|
||||||
- **`src/mcp_office_tools/utils/`**: Utility modules for validation, caching, and file detection
|
- **`src/mcwaddams/utils/`**: Utility modules for validation, caching, and file detection
|
||||||
- **FastMCP Framework**: Uses FastMCP for MCP protocol implementation
|
- **FastMCP Framework**: Uses FastMCP for MCP protocol implementation
|
||||||
- **Multi-library approach**: Integrates python-docx, openpyxl, python-pptx, pandas, and legacy format handlers
|
- **Multi-library approach**: Integrates python-docx, openpyxl, python-pptx, pandas, and legacy format handlers
|
||||||
|
|
||||||
@ -165,8 +165,8 @@ Tools are registered using FastMCP decorators and follow MCP protocol standards
|
|||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
mcp-office-tools/
|
mcwaddams/
|
||||||
├── src/mcp_office_tools/
|
├── src/mcwaddams/
|
||||||
│ ├── __init__.py # Package initialization
|
│ ├── __init__.py # Package initialization
|
||||||
│ ├── server.py # Main FastMCP server with tools
|
│ ├── server.py # Main FastMCP server with tools
|
||||||
│ ├── utils/ # Utility modules
|
│ ├── utils/ # Utility modules
|
||||||
@ -218,7 +218,7 @@ The project uses pytest with:
|
|||||||
|
|
||||||
## Relationship to MCP PDF Tools
|
## Relationship to MCP PDF Tools
|
||||||
|
|
||||||
MCP Office Tools is designed as a companion to MCP PDF Tools:
|
mcwaddams is designed as a companion to MCP PDF Tools:
|
||||||
- Consistent API design patterns
|
- Consistent API design patterns
|
||||||
- Similar caching and URL handling
|
- Similar caching and URL handling
|
||||||
- Parallel tool organization
|
- Parallel tool organization
|
||||||
|
|||||||
@ -151,10 +151,10 @@ except OfficeFileError as e:
|
|||||||
|
|
||||||
### **Server Status: OPERATIONAL ✅**
|
### **Server Status: OPERATIONAL ✅**
|
||||||
```bash
|
```bash
|
||||||
$ uv run mcp-office-tools --version
|
$ uv run mcwaddams --version
|
||||||
MCP Office Tools v0.1.0
|
MCP Office Tools v0.1.0
|
||||||
|
|
||||||
$ uv run mcp-office-tools
|
$ uv run mcwaddams
|
||||||
[Server starts successfully with FastMCP banner]
|
[Server starts successfully with FastMCP banner]
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -178,8 +178,8 @@ $ uv run mcp-office-tools
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"mcp-office-tools": {
|
"mcwaddams": {
|
||||||
"command": "mcp-office-tools"
|
"command": "mcwaddams"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
6
Makefile
6
Makefile
@ -97,13 +97,13 @@ quick-test:
|
|||||||
# Coverage report
|
# Coverage report
|
||||||
coverage:
|
coverage:
|
||||||
@echo "📊 Generating coverage report..."
|
@echo "📊 Generating coverage report..."
|
||||||
@uv run pytest --cov=mcp_office_tools --cov-report=html --cov-report=term
|
@uv run pytest --cov=mcwaddams --cov-report=html --cov-report=term
|
||||||
@echo "✅ Coverage report generated at htmlcov/index.html"
|
@echo "✅ Coverage report generated at htmlcov/index.html"
|
||||||
|
|
||||||
# Run server in development mode
|
# Run server in development mode
|
||||||
dev:
|
dev:
|
||||||
@echo "🚀 Starting MCP Office Tools server..."
|
@echo "🚀 Starting MCP Office Tools server..."
|
||||||
@uv run mcp-office-tools
|
@uv run mcwaddams
|
||||||
|
|
||||||
# Build distribution packages
|
# Build distribution packages
|
||||||
build:
|
build:
|
||||||
@ -116,7 +116,7 @@ info:
|
|||||||
@echo "MCP Office Tools - Project Information"
|
@echo "MCP Office Tools - Project Information"
|
||||||
@echo "======================================="
|
@echo "======================================="
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Project: mcp-office-tools"
|
@echo "Project: mcwaddams"
|
||||||
@echo "Version: $(shell grep '^version' pyproject.toml | cut -d'"' -f2)"
|
@echo "Version: $(shell grep '^version' pyproject.toml | cut -d'"' -f2)"
|
||||||
@echo "Python: $(shell python --version)"
|
@echo "Python: $(shell python --version)"
|
||||||
@echo "UV: $(shell uv --version 2>/dev/null || echo 'not installed')"
|
@echo "UV: $(shell uv --version 2>/dev/null || echo 'not installed')"
|
||||||
|
|||||||
213
README.md
213
README.md
@ -1,15 +1,15 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
# 📊 MCP Office Tools
|
# 📎 mcwaddams
|
||||||
|
|
||||||
**MCP server for extracting text, tables, images, and data from Microsoft Office files**
|
**MCP server for Microsoft Office document processing**
|
||||||
|
|
||||||
[](https://www.python.org/downloads/)
|
[](https://www.python.org/downloads/)
|
||||||
[](https://gofastmcp.com)
|
[](https://gofastmcp.com)
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
[](https://modelcontextprotocol.io)
|
[](https://modelcontextprotocol.io)
|
||||||
|
|
||||||
*Word, Excel, PowerPoint, CSV — all the formats your AI agent needs to read but can't*
|
*"I was told there would be document extraction."*
|
||||||
|
|
||||||
[Installation](#-installation) • [Tools](#-available-tools) • [Examples](#-usage-examples) • [Testing](#-testing)
|
[Installation](#-installation) • [Tools](#-available-tools) • [Examples](#-usage-examples) • [Testing](#-testing)
|
||||||
|
|
||||||
@ -17,14 +17,22 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## The Backstory
|
||||||
|
|
||||||
|
Milton Waddams was relocated to the basement. They took his stapler. But down there, surrounded by boxes of `.doc` files from 1997 and `.xls` spreadsheets that predate Unicode, he became something else entirely: a document processing expert.
|
||||||
|
|
||||||
|
This MCP server channels that energy. It handles the legacy formats nobody else wants to touch. It extracts text from files that should have been migrated to Google Docs a decade ago. It reads the TPS reports.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|
||||||
- **Universal extraction** — Pull text, images, and metadata from any Office format
|
- **Universal extraction** — Pull text, images, and metadata from any Office format
|
||||||
- **Format-specific tools** — Deep analysis for Word (tables, structure), Excel (formulas, charts), PowerPoint
|
- **Format-specific tools** — Deep analysis for Word (tables, structure), Excel (formulas, charts), PowerPoint
|
||||||
- **Automatic pagination** — Large documents get chunked so they don't blow up your context window
|
- **Automatic pagination** — Large documents get chunked so they don't blow up your context window
|
||||||
- **Fallback processing** — When one library chokes on a weird file, we try another. No silent failures.
|
- **Fallback processing** — When one library chokes on a weird file, we try another
|
||||||
- **URL support** — Pass a URL instead of a file path; we'll download and cache it
|
- **URL support** — Pass a URL instead of a file path; we'll download and cache it
|
||||||
- **Legacy formats** — Yes, even those .doc and .xls files from 2003 still work
|
- **Legacy formats** — Yes, even those `.doc` and `.xls` files from the basement
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -32,11 +40,11 @@
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Quick install with uvx (recommended)
|
# Quick install with uvx (recommended)
|
||||||
uvx mcp-office-tools
|
uvx mcwaddams
|
||||||
|
|
||||||
# Or install with uv/pip
|
# Or install with uv/pip
|
||||||
uv add mcp-office-tools
|
uv add mcwaddams
|
||||||
pip install mcp-office-tools
|
pip install mcwaddams
|
||||||
```
|
```
|
||||||
|
|
||||||
### Claude Desktop Configuration
|
### Claude Desktop Configuration
|
||||||
@ -46,9 +54,9 @@ Add to your `claude_desktop_config.json`:
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"office-tools": {
|
"mcwaddams": {
|
||||||
"command": "uvx",
|
"command": "uvx",
|
||||||
"args": ["mcp-office-tools"]
|
"args": ["mcwaddams"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -57,7 +65,7 @@ Add to your `claude_desktop_config.json`:
|
|||||||
### Claude Code Configuration
|
### Claude Code Configuration
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
claude mcp add office-tools "uvx mcp-office-tools"
|
claude mcp add mcwaddams "uvx mcwaddams"
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -183,7 +191,7 @@ Use `text_patterns_only=True` to skip heading style detection for documents with
|
|||||||
|
|
||||||
## 🎯 MCP Prompts
|
## 🎯 MCP Prompts
|
||||||
|
|
||||||
Pre-built workflows that chain multiple tools together. Use these as starting points:
|
Pre-built workflows that chain multiple tools together:
|
||||||
|
|
||||||
| Prompt | Level | Description |
|
| Prompt | Level | Description |
|
||||||
|--------|-------|-------------|
|
|--------|-------|-------------|
|
||||||
@ -245,99 +253,7 @@ result = await analyze_excel_data(
|
|||||||
check_data_quality=True
|
check_data_quality=True
|
||||||
)
|
)
|
||||||
|
|
||||||
# Returns per-column analysis
|
# Returns per-column analysis with quality issues
|
||||||
# {
|
|
||||||
# "analysis": {
|
|
||||||
# "Sheet1": {
|
|
||||||
# "dimensions": {"rows": 1000, "columns": 12},
|
|
||||||
# "column_info": {
|
|
||||||
# "Revenue": {
|
|
||||||
# "data_type": "float64",
|
|
||||||
# "null_percentage": 2.3,
|
|
||||||
# "statistics": {"mean": 45000, "median": 42000, ...},
|
|
||||||
# "quality_issues": ["5 potential outliers"]
|
|
||||||
# }
|
|
||||||
# },
|
|
||||||
# "data_quality": {
|
|
||||||
# "completeness_percentage": 97.8,
|
|
||||||
# "duplicate_rows": 12
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
```
|
|
||||||
|
|
||||||
### Extract Excel Formulas
|
|
||||||
|
|
||||||
```python
|
|
||||||
result = await extract_excel_formulas(
|
|
||||||
file_path="financial-model.xlsx",
|
|
||||||
analyze_dependencies=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Returns formula details with dependency mapping
|
|
||||||
# {
|
|
||||||
# "formulas": {
|
|
||||||
# "Sheet1": [
|
|
||||||
# {
|
|
||||||
# "cell": "D2",
|
|
||||||
# "formula": "=B2*C2",
|
|
||||||
# "value": 1500.00,
|
|
||||||
# "dependencies": ["B2", "C2"]
|
|
||||||
# }
|
|
||||||
# ]
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
```
|
|
||||||
|
|
||||||
### Generate Chart Data
|
|
||||||
|
|
||||||
```python
|
|
||||||
result = await create_excel_chart_data(
|
|
||||||
file_path="quarterly-revenue.xlsx",
|
|
||||||
chart_type="line",
|
|
||||||
output_format="chartjs"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Returns ready-to-use Chart.js configuration
|
|
||||||
# {
|
|
||||||
# "chartjs": {
|
|
||||||
# "type": "line",
|
|
||||||
# "data": {
|
|
||||||
# "labels": ["Q1", "Q2", "Q3", "Q4"],
|
|
||||||
# "datasets": [{"label": "Revenue", "data": [100, 120, 115, 140]}]
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
```
|
|
||||||
|
|
||||||
### Extract Word Tables
|
|
||||||
|
|
||||||
```python
|
|
||||||
result = await extract_word_tables(
|
|
||||||
file_path="contract.docx",
|
|
||||||
output_format="markdown"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Returns tables with optional format conversion
|
|
||||||
# {
|
|
||||||
# "tables": [
|
|
||||||
# {
|
|
||||||
# "table_index": 0,
|
|
||||||
# "dimensions": {"rows": 5, "columns": 3},
|
|
||||||
# "converted_output": "| Name | Role | Department |\n|---|---|---|\n..."
|
|
||||||
# }
|
|
||||||
# ]
|
|
||||||
# }
|
|
||||||
```
|
|
||||||
|
|
||||||
### Process Documents from URLs
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Documents are downloaded and cached automatically
|
|
||||||
result = await extract_text("https://example.com/report.docx")
|
|
||||||
|
|
||||||
# Cache expires after 1 hour by default
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Index Document for On-Demand Resource Fetching
|
### Index Document for On-Demand Resource Fetching
|
||||||
@ -351,8 +267,7 @@ result = await index_document("novel.docx")
|
|||||||
# "doc_id": "56036b0f171a",
|
# "doc_id": "56036b0f171a",
|
||||||
# "resources": {
|
# "resources": {
|
||||||
# "chapter": [
|
# "chapter": [
|
||||||
# {"id": "1", "title": "Chapter 1: The Beginning", "uri": "chapter://56036b0f171a/1"},
|
# {"id": "1", "title": "Chapter 1", "uri": "chapter://56036b0f171a/1"},
|
||||||
# {"id": "2", "title": "Chapter 2: Rising Action", "uri": "chapter://56036b0f171a/2"},
|
|
||||||
# ...
|
# ...
|
||||||
# ],
|
# ],
|
||||||
# "image": [
|
# "image": [
|
||||||
@ -362,63 +277,47 @@ result = await index_document("novel.docx")
|
|||||||
# }
|
# }
|
||||||
# }
|
# }
|
||||||
|
|
||||||
# Now fetch specific content via MCP resources:
|
# Fetch specific content via MCP resources:
|
||||||
# - chapter://56036b0f171a/1 → Chapter 1 as markdown
|
# - chapter://56036b0f171a/1 → Chapter 1 as markdown
|
||||||
# - chapter://56036b0f171a/1.txt → Chapter 1 as plain text
|
# - chapter://56036b0f171a/1.txt → Chapter 1 as plain text
|
||||||
# - chapters://56036b0f171a/1-3 → Chapters 1-3 combined
|
# - chapters://56036b0f171a/1-3 → Chapters 1-3 combined
|
||||||
# - image://56036b0f171a/0 → First embedded image
|
|
||||||
|
|
||||||
# Works with Excel and PowerPoint too:
|
|
||||||
await index_document("data.xlsx")
|
|
||||||
# → sheet://abc123/Revenue, sheet://abc123/Expenses, ...
|
|
||||||
|
|
||||||
await index_document("presentation.pptx")
|
|
||||||
# → slide://def456/1, slide://def456/2, ...
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🧪 Testing
|
## 🧪 Testing
|
||||||
|
|
||||||
We built a visual test dashboard because staring at pytest output gets old. Run `make test` and you get an HTML report with pass/fail stats, detailed I/O for each test, and expandable tracebacks when things break.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run tests and generate the dashboard
|
# Run tests and generate the dashboard
|
||||||
make test
|
make test
|
||||||
|
|
||||||
# Just pytest, no dashboard
|
# Just pytest
|
||||||
make test-pytest
|
make test-pytest
|
||||||
|
|
||||||
# Open existing dashboard
|
# Open dashboard
|
||||||
make view-dashboard
|
make view-dashboard
|
||||||
```
|
```
|
||||||
|
|
||||||
The dashboard has an MS Office-inspired theme (Word blue, Excel green, PowerPoint orange) and groups tests by category so you can see what's working at a glance.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🏗 Architecture
|
## 🏗 Architecture
|
||||||
|
|
||||||
The mixin pattern keeps things modular — universal tools work on everything, format-specific tools go deeper. When the primary library can't handle something (corrupted files, weird formatting), we fall back to alternatives.
|
The mixin pattern keeps things modular — universal tools work on everything, format-specific tools go deeper.
|
||||||
|
|
||||||
```
|
```
|
||||||
mcp-office-tools/
|
mcwaddams/
|
||||||
├── src/mcp_office_tools/
|
├── src/mcwaddams/
|
||||||
│ ├── server.py # FastMCP server + resource templates
|
│ ├── server.py # FastMCP server + resource templates
|
||||||
│ ├── resources.py # Resource store for on-demand content
|
│ ├── resources.py # Resource store for on-demand content
|
||||||
│ ├── mixins/
|
│ ├── mixins/
|
||||||
│ │ ├── universal.py # Format-agnostic tools (incl. index_document)
|
│ │ ├── universal.py # Format-agnostic tools
|
||||||
│ │ ├── word.py # Word-specific tools
|
│ │ ├── word.py # Word-specific tools
|
||||||
│ │ ├── excel.py # Excel-specific tools
|
│ │ ├── excel.py # Excel-specific tools
|
||||||
│ │ └── powerpoint.py # PowerPoint tools (WIP)
|
│ │ └── powerpoint.py # PowerPoint tools
|
||||||
│ ├── utils/
|
│ ├── utils/ # Validation, caching, detection
|
||||||
│ │ ├── validation.py # File validation
|
|
||||||
│ │ ├── file_detection.py # Format detection
|
|
||||||
│ │ ├── caching.py # URL caching
|
|
||||||
│ │ └── decorators.py # Error handling, defaults
|
|
||||||
│ └── pagination.py # Large document pagination
|
│ └── pagination.py # Large document pagination
|
||||||
├── tests/ # pytest test suite
|
├── tests/
|
||||||
└── reports/ # Test dashboard output
|
└── reports/
|
||||||
```
|
```
|
||||||
|
|
||||||
### Processing Libraries
|
### Processing Libraries
|
||||||
@ -436,57 +335,17 @@ mcp-office-tools/
|
|||||||
## 🔧 Development
|
## 🔧 Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Clone and install
|
git clone https://github.com/ryanmalloy/mcwaddams.git
|
||||||
git clone https://github.com/yourusername/mcp-office-tools.git
|
cd mcwaddams
|
||||||
cd mcp-office-tools
|
|
||||||
uv sync --dev
|
uv sync --dev
|
||||||
|
|
||||||
# Run tests
|
|
||||||
uv run pytest
|
uv run pytest
|
||||||
|
|
||||||
# Format and lint
|
|
||||||
uv run black src/ tests/
|
uv run black src/ tests/
|
||||||
uv run ruff check src/ tests/
|
uv run ruff check src/ tests/
|
||||||
|
|
||||||
# Type check
|
|
||||||
uv run mypy src/
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📦 Dependencies
|
|
||||||
|
|
||||||
**Core:**
|
|
||||||
- `fastmcp` - MCP server framework
|
|
||||||
- `python-docx` - Word document processing
|
|
||||||
- `openpyxl` - Excel spreadsheet processing
|
|
||||||
- `python-pptx` - PowerPoint processing
|
|
||||||
- `pandas` - Data analysis and CSV handling
|
|
||||||
- `mammoth` - Word to HTML/Markdown conversion
|
|
||||||
- `olefile` - Legacy OLE format support
|
|
||||||
- `xlrd` - Legacy Excel support
|
|
||||||
- `pillow` - Image processing
|
|
||||||
- `aiohttp` / `aiofiles` - Async HTTP and file I/O
|
|
||||||
|
|
||||||
**Optional:**
|
|
||||||
- `python-magic` - Enhanced MIME type detection
|
|
||||||
- `msoffcrypto-tool` - Encrypted file detection
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🤝 Related Projects
|
|
||||||
|
|
||||||
- **[MCP PDF Tools](https://github.com/yourusername/mcp-pdf-tools)** - Companion server for PDF processing
|
|
||||||
- **[FastMCP](https://gofastmcp.com)** - The framework powering this server
|
|
||||||
|
|
||||||
## 📝 Behind the Scenes
|
|
||||||
|
|
||||||
This README was rewritten during a human-AI collaboration session. The process raised questions about discernment, voice, and what makes documentation actually land:
|
|
||||||
|
|
||||||
- **[AI Isn't New. Your Discernment Is What Matters.](https://ryanmalloy.com/blog/ai-discernment)** — Ryan's take on 40 years of writing code and why discernment matters more than the tools
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📜 License
|
## 📜 License
|
||||||
|
|
||||||
MIT License - see [LICENSE](LICENSE) for details.
|
MIT License - see [LICENSE](LICENSE) for details.
|
||||||
@ -495,6 +354,8 @@ MIT License - see [LICENSE](LICENSE) for details.
|
|||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
|
*Named for Milton Waddams, who was relocated to the basement with the legacy documents.*
|
||||||
|
|
||||||
**Built with [FastMCP](https://gofastmcp.com) and the [Model Context Protocol](https://modelcontextprotocol.io)**
|
**Built with [FastMCP](https://gofastmcp.com) and the [Model Context Protocol](https://modelcontextprotocol.io)**
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -59,7 +59,7 @@ async def test_tool_functionality():
|
|||||||
mixin = UniversalMixin(app)
|
mixin = UniversalMixin(app)
|
||||||
|
|
||||||
# Mock dependencies
|
# Mock dependencies
|
||||||
with patch('mcp_office_tools.utils.validation.validate_office_file'):
|
with patch('mcwaddams.utils.validation.validate_office_file'):
|
||||||
# Test tool directly through mixin
|
# Test tool directly through mixin
|
||||||
result = await mixin.extract_text("/test.csv")
|
result = await mixin.extract_text("/test.csv")
|
||||||
assert "text" in result
|
assert "text" in result
|
||||||
@ -185,13 +185,13 @@ uv run pytest
|
|||||||
uv run pytest tests/test_universal_mixin.py -v
|
uv run pytest tests/test_universal_mixin.py -v
|
||||||
|
|
||||||
# With coverage
|
# With coverage
|
||||||
uv run pytest --cov=mcp_office_tools
|
uv run pytest --cov=mcwaddams
|
||||||
```
|
```
|
||||||
|
|
||||||
### Continuous Integration
|
### Continuous Integration
|
||||||
```bash
|
```bash
|
||||||
# All tests with coverage reporting
|
# All tests with coverage reporting
|
||||||
uv run pytest --cov=mcp_office_tools --cov-report=xml --cov-report=html
|
uv run pytest --cov=mcwaddams --cov-report=xml --cov-report=html
|
||||||
```
|
```
|
||||||
|
|
||||||
## Key Testing Fixtures
|
## Key Testing Fixtures
|
||||||
|
|||||||
@ -17,7 +17,7 @@ This document captures critical bugs discovered and fixed while processing compl
|
|||||||
|
|
||||||
## 1. FastMCP Banner Corruption
|
## 1. FastMCP Banner Corruption
|
||||||
|
|
||||||
**File:** `src/mcp_office_tools/server.py`
|
**File:** `src/mcwaddams/server.py`
|
||||||
|
|
||||||
**Symptom:** MCP connection fails with `Invalid JSON: EOF while parsing`
|
**Symptom:** MCP connection fails with `Invalid JSON: EOF while parsing`
|
||||||
|
|
||||||
@ -35,7 +35,7 @@ def main():
|
|||||||
|
|
||||||
## 2. Page Range Cap Bug
|
## 2. Page Range Cap Bug
|
||||||
|
|
||||||
**File:** `src/mcp_office_tools/utils/word_processing.py`
|
**File:** `src/mcwaddams/utils/word_processing.py`
|
||||||
|
|
||||||
**Symptom:** Requesting pages 1-5 returns truncated content, but pages 195-200 returns everything.
|
**Symptom:** Requesting pages 1-5 returns truncated content, but pages 195-200 returns everything.
|
||||||
|
|
||||||
@ -57,7 +57,7 @@ max_chars = num_pages_requested * 50000
|
|||||||
|
|
||||||
## 3. Heading Scan Limit Bug
|
## 3. Heading Scan Limit Bug
|
||||||
|
|
||||||
**File:** `src/mcp_office_tools/utils/word_processing.py`
|
**File:** `src/mcwaddams/utils/word_processing.py`
|
||||||
|
|
||||||
**Symptom:** `_get_available_headings()` returns empty list for documents with chapters beyond the first few pages.
|
**Symptom:** `_get_available_headings()` returns empty list for documents with chapters beyond the first few pages.
|
||||||
|
|
||||||
@ -81,7 +81,7 @@ for element in doc.element.body: # Scan ALL elements
|
|||||||
|
|
||||||
## 4. Short-Text Fallback Logic Bug
|
## 4. Short-Text Fallback Logic Bug
|
||||||
|
|
||||||
**File:** `src/mcp_office_tools/utils/word_processing.py`
|
**File:** `src/mcwaddams/utils/word_processing.py`
|
||||||
|
|
||||||
**Symptom:** Chapter search fails even when chapter text exists and is under 100 characters.
|
**Symptom:** Chapter search fails even when chapter text exists and is under 100 characters.
|
||||||
|
|
||||||
@ -115,7 +115,7 @@ if is_heading_style or len(text_content.strip()) < 100:
|
|||||||
|
|
||||||
## 5. Critical xpath API Mismatch (ROOT CAUSE)
|
## 5. Critical xpath API Mismatch (ROOT CAUSE)
|
||||||
|
|
||||||
**File:** `src/mcp_office_tools/utils/word_processing.py`
|
**File:** `src/mcwaddams/utils/word_processing.py`
|
||||||
|
|
||||||
**Symptom:** Chapter search always returns "not found" even for chapters that clearly exist.
|
**Symptom:** Chapter search always returns "not found" even for chapters that clearly exist.
|
||||||
|
|
||||||
@ -152,7 +152,7 @@ if pPr is not None:
|
|||||||
|
|
||||||
## 6. Image Mode Default
|
## 6. Image Mode Default
|
||||||
|
|
||||||
**File:** `src/mcp_office_tools/mixins/word.py`
|
**File:** `src/mcwaddams/mixins/word.py`
|
||||||
|
|
||||||
**Symptom:** Responses exceed token limits when documents contain images.
|
**Symptom:** Responses exceed token limits when documents contain images.
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@ from pathlib import Path
|
|||||||
# Add the package to Python path for local testing
|
# Add the package to Python path for local testing
|
||||||
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
||||||
|
|
||||||
from mcp_office_tools.server import (
|
from mcwaddams.server import (
|
||||||
extract_text,
|
extract_text,
|
||||||
extract_images,
|
extract_images,
|
||||||
extract_metadata,
|
extract_metadata,
|
||||||
|
|||||||
@ -29,15 +29,15 @@ def test_import():
|
|||||||
print("🔍 Testing package import...")
|
print("🔍 Testing package import...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import mcp_office_tools
|
import mcwaddams
|
||||||
print(f"✅ Package imported successfully - Version: {mcp_office_tools.__version__}")
|
print(f"✅ Package imported successfully - Version: {mcwaddams.__version__}")
|
||||||
|
|
||||||
# Test server import
|
# Test server import
|
||||||
from mcp_office_tools.server import app
|
from mcwaddams.server import app
|
||||||
print("✅ Server module imported successfully")
|
print("✅ Server module imported successfully")
|
||||||
|
|
||||||
# Test utils import
|
# Test utils import
|
||||||
from mcp_office_tools.utils import OfficeFileError, get_supported_extensions
|
from mcwaddams.utils import OfficeFileError, get_supported_extensions
|
||||||
print("✅ Utils module imported successfully")
|
print("✅ Utils module imported successfully")
|
||||||
|
|
||||||
# Test supported extensions
|
# Test supported extensions
|
||||||
@ -58,7 +58,7 @@ async def test_utils():
|
|||||||
print("\n🔧 Testing utility functions...")
|
print("\n🔧 Testing utility functions...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from mcp_office_tools.utils import (
|
from mcwaddams.utils import (
|
||||||
detect_file_format,
|
detect_file_format,
|
||||||
validate_office_path,
|
validate_office_path,
|
||||||
OfficeFileError
|
OfficeFileError
|
||||||
@ -103,7 +103,7 @@ def test_server_structure():
|
|||||||
print("\n🖥️ Testing server structure...")
|
print("\n🖥️ Testing server structure...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from mcp_office_tools.server import app
|
from mcwaddams.server import app
|
||||||
|
|
||||||
# Check that app has tools
|
# Check that app has tools
|
||||||
if hasattr(app, '_tools'):
|
if hasattr(app, '_tools'):
|
||||||
@ -134,7 +134,7 @@ async def test_caching():
|
|||||||
print("\n📦 Testing caching functionality...")
|
print("\n📦 Testing caching functionality...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from mcp_office_tools.utils.caching import OfficeFileCache, get_cache
|
from mcwaddams.utils.caching import OfficeFileCache, get_cache
|
||||||
|
|
||||||
# Test cache creation
|
# Test cache creation
|
||||||
cache = get_cache()
|
cache = get_cache()
|
||||||
@ -145,7 +145,7 @@ async def test_caching():
|
|||||||
print(f"✅ Cache stats: {stats['total_files']} files, {stats['total_size_mb']} MB")
|
print(f"✅ Cache stats: {stats['total_files']} files, {stats['total_size_mb']} MB")
|
||||||
|
|
||||||
# Test URL validation
|
# Test URL validation
|
||||||
from mcp_office_tools.utils.validation import is_url
|
from mcwaddams.utils.validation import is_url
|
||||||
|
|
||||||
assert is_url("https://example.com/file.docx")
|
assert is_url("https://example.com/file.docx")
|
||||||
assert not is_url("/local/path/file.docx")
|
assert not is_url("/local/path/file.docx")
|
||||||
@ -243,7 +243,7 @@ async def main():
|
|||||||
print("🎉 Installation verified successfully!")
|
print("🎉 Installation verified successfully!")
|
||||||
print("✅ MCP Office Tools is ready to use.")
|
print("✅ MCP Office Tools is ready to use.")
|
||||||
print("\n🚀 Next steps:")
|
print("\n🚀 Next steps:")
|
||||||
print(" 1. Run the MCP server: uv run mcp-office-tools")
|
print(" 1. Run the MCP server: uv run mcwaddams")
|
||||||
print(" 2. Add to Claude Desktop config")
|
print(" 2. Add to Claude Desktop config")
|
||||||
print(" 3. Test with Office documents")
|
print(" 3. Test with Office documents")
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "mcp-office-tools"
|
name = "mcwaddams"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "MCP server for comprehensive Microsoft Office document processing"
|
description = "MCP server for Microsoft Office document processing. Named for Milton Waddams, who was relocated to the basement with boxes of legacy documents."
|
||||||
authors = [{name = "MCP Office Tools", email = "contact@mcpofficetools.dev"}]
|
authors = [{name = "Ryan Malloy", email = "ryan@supported.systems"}]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = {text = "MIT"}
|
license = {text = "MIT"}
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
keywords = ["mcp", "office", "docx", "xlsx", "pptx", "word", "excel", "powerpoint", "document", "processing"]
|
keywords = ["mcp", "office", "docx", "xlsx", "pptx", "word", "excel", "powerpoint", "document", "processing", "milton", "legacy"]
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Development Status :: 4 - Beta",
|
"Development Status :: 4 - Beta",
|
||||||
"Intended Audience :: Developers",
|
"Intended Audience :: Developers",
|
||||||
@ -64,20 +64,19 @@ enhanced = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
Homepage = "https://github.com/mcp-office-tools/mcp-office-tools"
|
Homepage = "https://github.com/ryanmalloy/mcwaddams"
|
||||||
Documentation = "https://mcp-office-tools.readthedocs.io"
|
Repository = "https://github.com/ryanmalloy/mcwaddams"
|
||||||
Repository = "https://github.com/mcp-office-tools/mcp-office-tools"
|
Issues = "https://github.com/ryanmalloy/mcwaddams/issues"
|
||||||
Issues = "https://github.com/mcp-office-tools/mcp-office-tools/issues"
|
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
mcp-office-tools = "mcp_office_tools.server:main"
|
mcwaddams = "mcwaddams.server:main"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["hatchling"]
|
requires = ["hatchling"]
|
||||||
build-backend = "hatchling.build"
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
[tool.hatch.build.targets.wheel]
|
[tool.hatch.build.targets.wheel]
|
||||||
packages = ["src/mcp_office_tools"]
|
packages = ["src/mcwaddams"]
|
||||||
|
|
||||||
[tool.hatch.build.targets.sdist]
|
[tool.hatch.build.targets.sdist]
|
||||||
include = [
|
include = [
|
||||||
@ -145,7 +144,7 @@ minversion = "7.0"
|
|||||||
addopts = [
|
addopts = [
|
||||||
"--strict-markers",
|
"--strict-markers",
|
||||||
"--strict-config",
|
"--strict-config",
|
||||||
"--cov=mcp_office_tools",
|
"--cov=mcwaddams",
|
||||||
"--cov-report=term-missing",
|
"--cov-report=term-missing",
|
||||||
"--cov-report=html",
|
"--cov-report=html",
|
||||||
"--cov-report=xml",
|
"--cov-report=xml",
|
||||||
@ -161,7 +160,7 @@ markers = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[tool.coverage.run]
|
[tool.coverage.run]
|
||||||
source = ["src/mcp_office_tools"]
|
source = ["src/mcwaddams"]
|
||||||
omit = [
|
omit = [
|
||||||
"*/tests/*",
|
"*/tests/*",
|
||||||
"*/test_*",
|
"*/test_*",
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"start_time": "2026-01-11T09:40:29.164041",
|
"start_time": "2026-01-11T11:35:12.792077",
|
||||||
"pytest_version": "9.0.2",
|
"pytest_version": "9.0.2",
|
||||||
"end_time": "2026-01-11T09:40:30.048909",
|
"end_time": "2026-01-11T11:35:14.191660",
|
||||||
"duration": 0.8848621845245361,
|
"duration": 1.399580955505371,
|
||||||
"exit_status": 0
|
"exit_status": 0
|
||||||
},
|
},
|
||||||
"summary": {
|
"summary": {
|
||||||
|
|||||||
@ -39,7 +39,7 @@ async def test_mcp_server():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Import the server components
|
# Import the server components
|
||||||
from mcp_office_tools.mixins import UniversalMixin
|
from mcwaddams.mixins import UniversalMixin
|
||||||
|
|
||||||
# Test the Universal Mixin directly
|
# Test the Universal Mixin directly
|
||||||
universal = UniversalMixin()
|
universal = UniversalMixin()
|
||||||
|
|||||||
@ -12,9 +12,9 @@ def test_pagination():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Import the server components
|
# Import the server components
|
||||||
from mcp_office_tools.server import app
|
from mcwaddams.server import app
|
||||||
from mcp_office_tools.mixins.word import WordMixin
|
from mcwaddams.mixins.word import WordMixin
|
||||||
from mcp_office_tools.pagination import DocumentPaginationManager, paginate_document_conversion
|
from mcwaddams.pagination import DocumentPaginationManager, paginate_document_conversion
|
||||||
|
|
||||||
print("✅ Successfully imported all pagination components:")
|
print("✅ Successfully imported all pagination components:")
|
||||||
print(" • DocumentPaginationManager")
|
print(" • DocumentPaginationManager")
|
||||||
|
|||||||
@ -14,7 +14,7 @@ from typing import Dict, Any
|
|||||||
from fastmcp import FastMCP
|
from fastmcp import FastMCP
|
||||||
# FastMCP testing utilities are created manually
|
# FastMCP testing utilities are created manually
|
||||||
|
|
||||||
from mcp_office_tools.mixins import UniversalMixin, WordMixin, ExcelMixin, PowerPointMixin
|
from mcwaddams.mixins import UniversalMixin, WordMixin, ExcelMixin, PowerPointMixin
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@ -211,23 +211,23 @@ class MockValidationContext:
|
|||||||
self.patches = []
|
self.patches = []
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
import mcp_office_tools.utils.validation
|
import mcwaddams.utils.validation
|
||||||
import mcp_office_tools.utils.file_detection
|
import mcwaddams.utils.file_detection
|
||||||
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
if self.resolve_path:
|
if self.resolve_path:
|
||||||
p1 = patch('mcp_office_tools.utils.validation.resolve_office_file_path',
|
p1 = patch('mcwaddams.utils.validation.resolve_office_file_path',
|
||||||
return_value=self.resolve_path)
|
return_value=self.resolve_path)
|
||||||
self.patches.append(p1)
|
self.patches.append(p1)
|
||||||
p1.start()
|
p1.start()
|
||||||
|
|
||||||
p2 = patch('mcp_office_tools.utils.validation.validate_office_file',
|
p2 = patch('mcwaddams.utils.validation.validate_office_file',
|
||||||
return_value=self.validation_result)
|
return_value=self.validation_result)
|
||||||
self.patches.append(p2)
|
self.patches.append(p2)
|
||||||
p2.start()
|
p2.start()
|
||||||
|
|
||||||
p3 = patch('mcp_office_tools.utils.file_detection.detect_format',
|
p3 = patch('mcwaddams.utils.file_detection.detect_format',
|
||||||
return_value=self.format_detection)
|
return_value=self.format_detection)
|
||||||
self.patches.append(p3)
|
self.patches.append(p3)
|
||||||
p3.start()
|
p3.start()
|
||||||
|
|||||||
@ -20,8 +20,8 @@ from typing import Dict, Any
|
|||||||
from fastmcp import FastMCP
|
from fastmcp import FastMCP
|
||||||
# FastMCP testing - using direct tool access
|
# FastMCP testing - using direct tool access
|
||||||
|
|
||||||
from mcp_office_tools.mixins import UniversalMixin, WordMixin, ExcelMixin, PowerPointMixin
|
from mcwaddams.mixins import UniversalMixin, WordMixin, ExcelMixin, PowerPointMixin
|
||||||
from mcp_office_tools.utils import OfficeFileError
|
from mcwaddams.utils import OfficeFileError
|
||||||
|
|
||||||
|
|
||||||
class TestMixinArchitecture:
|
class TestMixinArchitecture:
|
||||||
@ -131,9 +131,9 @@ class TestUniversalMixinUnit:
|
|||||||
await universal_mixin.extract_text("/nonexistent/file.docx")
|
await universal_mixin.extract_text("/nonexistent/file.docx")
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@patch('mcp_office_tools.mixins.universal.validate_office_file')
|
@patch('mcwaddams.mixins.universal.validate_office_file')
|
||||||
@patch('mcp_office_tools.mixins.universal.detect_format')
|
@patch('mcwaddams.mixins.universal.detect_format')
|
||||||
@patch('mcp_office_tools.mixins.universal.resolve_office_file_path')
|
@patch('mcwaddams.mixins.universal.resolve_office_file_path')
|
||||||
async def test_extract_text_csv_success(self, mock_resolve, mock_detect, mock_validate, universal_mixin, mock_csv_file):
|
async def test_extract_text_csv_success(self, mock_resolve, mock_detect, mock_validate, universal_mixin, mock_csv_file):
|
||||||
"""Test successful CSV text extraction with proper mocking."""
|
"""Test successful CSV text extraction with proper mocking."""
|
||||||
# Setup mocks
|
# Setup mocks
|
||||||
@ -200,9 +200,9 @@ class TestWordMixinUnit:
|
|||||||
await word_mixin.convert_to_markdown("/nonexistent/file.docx")
|
await word_mixin.convert_to_markdown("/nonexistent/file.docx")
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@patch('mcp_office_tools.mixins.word.validate_office_file')
|
@patch('mcwaddams.mixins.word.validate_office_file')
|
||||||
@patch('mcp_office_tools.mixins.word.detect_format')
|
@patch('mcwaddams.mixins.word.detect_format')
|
||||||
@patch('mcp_office_tools.mixins.word.resolve_office_file_path')
|
@patch('mcwaddams.mixins.word.resolve_office_file_path')
|
||||||
async def test_convert_to_markdown_non_word_document(self, mock_resolve, mock_detect, mock_validate, word_mixin):
|
async def test_convert_to_markdown_non_word_document(self, mock_resolve, mock_detect, mock_validate, word_mixin):
|
||||||
"""Test that non-Word documents are rejected for markdown conversion."""
|
"""Test that non-Word documents are rejected for markdown conversion."""
|
||||||
# Setup mocks for a non-Word document
|
# Setup mocks for a non-Word document
|
||||||
@ -287,9 +287,9 @@ class TestMockingStrategies:
|
|||||||
}
|
}
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@patch('mcp_office_tools.mixins.universal.resolve_office_file_path')
|
@patch('mcwaddams.mixins.universal.resolve_office_file_path')
|
||||||
@patch('mcp_office_tools.mixins.universal.validate_office_file')
|
@patch('mcwaddams.mixins.universal.validate_office_file')
|
||||||
@patch('mcp_office_tools.mixins.universal.detect_format')
|
@patch('mcwaddams.mixins.universal.detect_format')
|
||||||
async def test_comprehensive_mocking_pattern(self, mock_detect, mock_validate, mock_resolve, mock_office_file):
|
async def test_comprehensive_mocking_pattern(self, mock_detect, mock_validate, mock_resolve, mock_office_file):
|
||||||
"""Demonstrate comprehensive mocking pattern for tool testing."""
|
"""Demonstrate comprehensive mocking pattern for tool testing."""
|
||||||
app = FastMCP("Test App")
|
app = FastMCP("Test App")
|
||||||
@ -347,8 +347,8 @@ class TestFileOperationMocking:
|
|||||||
universal.register_all(app)
|
universal.register_all(app)
|
||||||
|
|
||||||
# Mock only the validation/detection layers
|
# Mock only the validation/detection layers
|
||||||
with patch('mcp_office_tools.utils.validation.validate_office_file') as mock_validate:
|
with patch('mcwaddams.utils.validation.validate_office_file') as mock_validate:
|
||||||
with patch('mcp_office_tools.utils.file_detection.detect_format') as mock_detect:
|
with patch('mcwaddams.utils.file_detection.detect_format') as mock_detect:
|
||||||
mock_validate.return_value = {"is_valid": True, "errors": []}
|
mock_validate.return_value = {"is_valid": True, "errors": []}
|
||||||
mock_detect.return_value = {
|
mock_detect.return_value = {
|
||||||
"category": "data",
|
"category": "data",
|
||||||
@ -375,9 +375,9 @@ class TestAsyncPatterns:
|
|||||||
universal.register_all(app)
|
universal.register_all(app)
|
||||||
|
|
||||||
# Mock all async dependencies
|
# Mock all async dependencies
|
||||||
with patch('mcp_office_tools.mixins.universal.resolve_office_file_path') as mock_resolve:
|
with patch('mcwaddams.mixins.universal.resolve_office_file_path') as mock_resolve:
|
||||||
with patch('mcp_office_tools.mixins.universal.validate_office_file') as mock_validate:
|
with patch('mcwaddams.mixins.universal.validate_office_file') as mock_validate:
|
||||||
with patch('mcp_office_tools.mixins.universal.detect_format') as mock_detect:
|
with patch('mcwaddams.mixins.universal.detect_format') as mock_detect:
|
||||||
# Make mocks properly async
|
# Make mocks properly async
|
||||||
mock_resolve.return_value = "/test.csv"
|
mock_resolve.return_value = "/test.csv"
|
||||||
mock_validate.return_value = {"is_valid": True, "errors": []}
|
mock_validate.return_value = {"is_valid": True, "errors": []}
|
||||||
|
|||||||
@ -8,8 +8,8 @@ from unittest.mock import patch, MagicMock
|
|||||||
|
|
||||||
# FastMCP testing - using direct tool access
|
# FastMCP testing - using direct tool access
|
||||||
|
|
||||||
from mcp_office_tools.server import app
|
from mcwaddams.server import app
|
||||||
from mcp_office_tools.utils import OfficeFileError
|
from mcwaddams.utils import OfficeFileError
|
||||||
|
|
||||||
|
|
||||||
class TestServerInitialization:
|
class TestServerInitialization:
|
||||||
@ -54,7 +54,7 @@ class TestServerInitialization:
|
|||||||
def test_mixin_composition_works(self):
|
def test_mixin_composition_works(self):
|
||||||
"""Test that mixin composition created the expected server structure."""
|
"""Test that mixin composition created the expected server structure."""
|
||||||
# Import the server module to ensure all mixins are initialized
|
# Import the server module to ensure all mixins are initialized
|
||||||
import mcp_office_tools.server as server_module
|
import mcwaddams.server as server_module
|
||||||
|
|
||||||
# Verify the mixins were created
|
# Verify the mixins were created
|
||||||
assert hasattr(server_module, 'universal_mixin')
|
assert hasattr(server_module, 'universal_mixin')
|
||||||
@ -63,7 +63,7 @@ class TestServerInitialization:
|
|||||||
assert hasattr(server_module, 'powerpoint_mixin')
|
assert hasattr(server_module, 'powerpoint_mixin')
|
||||||
|
|
||||||
# Verify mixin instances are correct types
|
# Verify mixin instances are correct types
|
||||||
from mcp_office_tools.mixins import UniversalMixin, WordMixin, ExcelMixin, PowerPointMixin
|
from mcwaddams.mixins import UniversalMixin, WordMixin, ExcelMixin, PowerPointMixin
|
||||||
assert isinstance(server_module.universal_mixin, UniversalMixin)
|
assert isinstance(server_module.universal_mixin, UniversalMixin)
|
||||||
assert isinstance(server_module.word_mixin, WordMixin)
|
assert isinstance(server_module.word_mixin, WordMixin)
|
||||||
assert isinstance(server_module.excel_mixin, ExcelMixin)
|
assert isinstance(server_module.excel_mixin, ExcelMixin)
|
||||||
|
|||||||
@ -16,8 +16,8 @@ from pathlib import Path
|
|||||||
from fastmcp import FastMCP
|
from fastmcp import FastMCP
|
||||||
# FastMCP testing - using direct tool access
|
# FastMCP testing - using direct tool access
|
||||||
|
|
||||||
from mcp_office_tools.mixins.universal import UniversalMixin
|
from mcwaddams.mixins.universal import UniversalMixin
|
||||||
from mcp_office_tools.utils import OfficeFileError
|
from mcwaddams.utils import OfficeFileError
|
||||||
|
|
||||||
|
|
||||||
class TestUniversalMixinRegistration:
|
class TestUniversalMixinRegistration:
|
||||||
@ -69,9 +69,9 @@ class TestExtractText:
|
|||||||
await mixin.extract_text("/nonexistent/file.docx")
|
await mixin.extract_text("/nonexistent/file.docx")
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@patch('mcp_office_tools.mixins.universal.resolve_office_file_path')
|
@patch('mcwaddams.mixins.universal.resolve_office_file_path')
|
||||||
@patch('mcp_office_tools.mixins.universal.validate_office_file')
|
@patch('mcwaddams.mixins.universal.validate_office_file')
|
||||||
@patch('mcp_office_tools.mixins.universal.detect_format')
|
@patch('mcwaddams.mixins.universal.detect_format')
|
||||||
async def test_extract_text_validation_failure(self, mock_detect, mock_validate, mock_resolve, mixin):
|
async def test_extract_text_validation_failure(self, mock_detect, mock_validate, mock_resolve, mixin):
|
||||||
"""Test extract_text with validation failure."""
|
"""Test extract_text with validation failure."""
|
||||||
mock_resolve.return_value = "/test.docx"
|
mock_resolve.return_value = "/test.docx"
|
||||||
@ -84,9 +84,9 @@ class TestExtractText:
|
|||||||
await mixin.extract_text("/test.docx")
|
await mixin.extract_text("/test.docx")
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@patch('mcp_office_tools.mixins.universal.resolve_office_file_path')
|
@patch('mcwaddams.mixins.universal.resolve_office_file_path')
|
||||||
@patch('mcp_office_tools.mixins.universal.validate_office_file')
|
@patch('mcwaddams.mixins.universal.validate_office_file')
|
||||||
@patch('mcp_office_tools.mixins.universal.detect_format')
|
@patch('mcwaddams.mixins.universal.detect_format')
|
||||||
async def test_extract_text_csv_success(self, mock_detect, mock_validate, mock_resolve, mixin):
|
async def test_extract_text_csv_success(self, mock_detect, mock_validate, mock_resolve, mixin):
|
||||||
"""Test successful CSV text extraction."""
|
"""Test successful CSV text extraction."""
|
||||||
# Setup mocks
|
# Setup mocks
|
||||||
@ -126,9 +126,9 @@ class TestExtractText:
|
|||||||
async def test_extract_text_parameter_handling(self, mixin):
|
async def test_extract_text_parameter_handling(self, mixin):
|
||||||
"""Test extract_text parameter validation and handling."""
|
"""Test extract_text parameter validation and handling."""
|
||||||
# Mock all dependencies for parameter testing
|
# Mock all dependencies for parameter testing
|
||||||
with patch('mcp_office_tools.mixins.universal.resolve_office_file_path') as mock_resolve:
|
with patch('mcwaddams.mixins.universal.resolve_office_file_path') as mock_resolve:
|
||||||
with patch('mcp_office_tools.mixins.universal.validate_office_file') as mock_validate:
|
with patch('mcwaddams.mixins.universal.validate_office_file') as mock_validate:
|
||||||
with patch('mcp_office_tools.mixins.universal.detect_format') as mock_detect:
|
with patch('mcwaddams.mixins.universal.detect_format') as mock_detect:
|
||||||
mock_resolve.return_value = "/test.docx"
|
mock_resolve.return_value = "/test.docx"
|
||||||
mock_validate.return_value = {"is_valid": True, "errors": []}
|
mock_validate.return_value = {"is_valid": True, "errors": []}
|
||||||
mock_detect.return_value = {"category": "word", "extension": ".docx", "format_name": "Word"}
|
mock_detect.return_value = {"category": "word", "extension": ".docx", "format_name": "Word"}
|
||||||
@ -174,9 +174,9 @@ class TestExtractImages:
|
|||||||
await mixin.extract_images("/nonexistent/file.docx")
|
await mixin.extract_images("/nonexistent/file.docx")
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@patch('mcp_office_tools.mixins.universal.resolve_office_file_path')
|
@patch('mcwaddams.mixins.universal.resolve_office_file_path')
|
||||||
@patch('mcp_office_tools.mixins.universal.validate_office_file')
|
@patch('mcwaddams.mixins.universal.validate_office_file')
|
||||||
@patch('mcp_office_tools.mixins.universal.detect_format')
|
@patch('mcwaddams.mixins.universal.detect_format')
|
||||||
async def test_extract_images_unsupported_format(self, mock_detect, mock_validate, mock_resolve, mixin):
|
async def test_extract_images_unsupported_format(self, mock_detect, mock_validate, mock_resolve, mixin):
|
||||||
"""Test extract_images with unsupported format (CSV) returns empty list."""
|
"""Test extract_images with unsupported format (CSV) returns empty list."""
|
||||||
mock_resolve.return_value = "/test.csv"
|
mock_resolve.return_value = "/test.csv"
|
||||||
@ -263,9 +263,9 @@ class TestDocumentHealth:
|
|||||||
return mixin
|
return mixin
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@patch('mcp_office_tools.mixins.universal.resolve_office_file_path')
|
@patch('mcwaddams.mixins.universal.resolve_office_file_path')
|
||||||
@patch('mcp_office_tools.mixins.universal.validate_office_file')
|
@patch('mcwaddams.mixins.universal.validate_office_file')
|
||||||
@patch('mcp_office_tools.mixins.universal.detect_format')
|
@patch('mcwaddams.mixins.universal.detect_format')
|
||||||
async def test_analyze_document_health_success(self, mock_detect, mock_validate, mock_resolve, mixin):
|
async def test_analyze_document_health_success(self, mock_detect, mock_validate, mock_resolve, mixin):
|
||||||
"""Test successful document health analysis."""
|
"""Test successful document health analysis."""
|
||||||
mock_resolve.return_value = "/test.docx"
|
mock_resolve.return_value = "/test.docx"
|
||||||
@ -343,9 +343,9 @@ class TestMockingPatterns:
|
|||||||
async def test_comprehensive_mocking_pattern(self, mixin):
|
async def test_comprehensive_mocking_pattern(self, mixin):
|
||||||
"""Demonstrate comprehensive mocking for complex tool testing."""
|
"""Demonstrate comprehensive mocking for complex tool testing."""
|
||||||
# Mock all external dependencies
|
# Mock all external dependencies
|
||||||
with patch('mcp_office_tools.mixins.universal.resolve_office_file_path') as mock_resolve:
|
with patch('mcwaddams.mixins.universal.resolve_office_file_path') as mock_resolve:
|
||||||
with patch('mcp_office_tools.mixins.universal.validate_office_file') as mock_validate:
|
with patch('mcwaddams.mixins.universal.validate_office_file') as mock_validate:
|
||||||
with patch('mcp_office_tools.mixins.universal.detect_format') as mock_detect:
|
with patch('mcwaddams.mixins.universal.detect_format') as mock_detect:
|
||||||
|
|
||||||
# Setup realistic mock responses
|
# Setup realistic mock responses
|
||||||
mock_resolve.return_value = "/realistic/path/document.docx"
|
mock_resolve.return_value = "/realistic/path/document.docx"
|
||||||
|
|||||||
@ -14,8 +14,8 @@ from pathlib import Path
|
|||||||
from fastmcp import FastMCP
|
from fastmcp import FastMCP
|
||||||
# FastMCP testing - using direct tool access
|
# FastMCP testing - using direct tool access
|
||||||
|
|
||||||
from mcp_office_tools.mixins.word import WordMixin
|
from mcwaddams.mixins.word import WordMixin
|
||||||
from mcp_office_tools.utils import OfficeFileError
|
from mcwaddams.utils import OfficeFileError
|
||||||
|
|
||||||
|
|
||||||
class TestWordMixinRegistration:
|
class TestWordMixinRegistration:
|
||||||
@ -58,9 +58,9 @@ class TestConvertToMarkdown:
|
|||||||
await mixin.convert_to_markdown("/nonexistent/file.docx")
|
await mixin.convert_to_markdown("/nonexistent/file.docx")
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@patch('mcp_office_tools.mixins.word.resolve_office_file_path')
|
@patch('mcwaddams.mixins.word.resolve_office_file_path')
|
||||||
@patch('mcp_office_tools.mixins.word.validate_office_file')
|
@patch('mcwaddams.mixins.word.validate_office_file')
|
||||||
@patch('mcp_office_tools.mixins.word.detect_format')
|
@patch('mcwaddams.mixins.word.detect_format')
|
||||||
async def test_convert_to_markdown_validation_failure(self, mock_detect, mock_validate, mock_resolve, mixin):
|
async def test_convert_to_markdown_validation_failure(self, mock_detect, mock_validate, mock_resolve, mixin):
|
||||||
"""Test convert_to_markdown with validation failure."""
|
"""Test convert_to_markdown with validation failure."""
|
||||||
mock_resolve.return_value = "/test.docx"
|
mock_resolve.return_value = "/test.docx"
|
||||||
@ -73,9 +73,9 @@ class TestConvertToMarkdown:
|
|||||||
await mixin.convert_to_markdown("/test.docx")
|
await mixin.convert_to_markdown("/test.docx")
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@patch('mcp_office_tools.mixins.word.resolve_office_file_path')
|
@patch('mcwaddams.mixins.word.resolve_office_file_path')
|
||||||
@patch('mcp_office_tools.mixins.word.validate_office_file')
|
@patch('mcwaddams.mixins.word.validate_office_file')
|
||||||
@patch('mcp_office_tools.mixins.word.detect_format')
|
@patch('mcwaddams.mixins.word.detect_format')
|
||||||
async def test_convert_to_markdown_non_word_document(self, mock_detect, mock_validate, mock_resolve, mixin):
|
async def test_convert_to_markdown_non_word_document(self, mock_detect, mock_validate, mock_resolve, mixin):
|
||||||
"""Test that non-Word documents are rejected."""
|
"""Test that non-Word documents are rejected."""
|
||||||
mock_resolve.return_value = "/test.xlsx"
|
mock_resolve.return_value = "/test.xlsx"
|
||||||
@ -90,9 +90,9 @@ class TestConvertToMarkdown:
|
|||||||
await mixin.convert_to_markdown("/test.xlsx")
|
await mixin.convert_to_markdown("/test.xlsx")
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@patch('mcp_office_tools.mixins.word.resolve_office_file_path')
|
@patch('mcwaddams.mixins.word.resolve_office_file_path')
|
||||||
@patch('mcp_office_tools.mixins.word.validate_office_file')
|
@patch('mcwaddams.mixins.word.validate_office_file')
|
||||||
@patch('mcp_office_tools.mixins.word.detect_format')
|
@patch('mcwaddams.mixins.word.detect_format')
|
||||||
async def test_convert_to_markdown_docx_success(self, mock_detect, mock_validate, mock_resolve, mixin):
|
async def test_convert_to_markdown_docx_success(self, mock_detect, mock_validate, mock_resolve, mixin):
|
||||||
"""Test successful DOCX to markdown conversion."""
|
"""Test successful DOCX to markdown conversion."""
|
||||||
# Setup mocks
|
# Setup mocks
|
||||||
@ -141,9 +141,9 @@ class TestConvertToMarkdown:
|
|||||||
async def test_convert_to_markdown_parameter_handling(self, mixin):
|
async def test_convert_to_markdown_parameter_handling(self, mixin):
|
||||||
"""Test convert_to_markdown parameter validation and handling."""
|
"""Test convert_to_markdown parameter validation and handling."""
|
||||||
# Mock all dependencies for parameter testing
|
# Mock all dependencies for parameter testing
|
||||||
with patch('mcp_office_tools.mixins.word.resolve_office_file_path') as mock_resolve:
|
with patch('mcwaddams.mixins.word.resolve_office_file_path') as mock_resolve:
|
||||||
with patch('mcp_office_tools.mixins.word.validate_office_file') as mock_validate:
|
with patch('mcwaddams.mixins.word.validate_office_file') as mock_validate:
|
||||||
with patch('mcp_office_tools.mixins.word.detect_format') as mock_detect:
|
with patch('mcwaddams.mixins.word.detect_format') as mock_detect:
|
||||||
mock_resolve.return_value = "/test.docx"
|
mock_resolve.return_value = "/test.docx"
|
||||||
mock_validate.return_value = {"is_valid": True, "errors": []}
|
mock_validate.return_value = {"is_valid": True, "errors": []}
|
||||||
mock_detect.return_value = {"category": "word", "extension": ".docx", "format_name": "Word"}
|
mock_detect.return_value = {"category": "word", "extension": ".docx", "format_name": "Word"}
|
||||||
@ -185,9 +185,9 @@ class TestConvertToMarkdown:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_convert_to_markdown_bookmark_priority(self, mixin):
|
async def test_convert_to_markdown_bookmark_priority(self, mixin):
|
||||||
"""Test that bookmark extraction takes priority over page ranges."""
|
"""Test that bookmark extraction takes priority over page ranges."""
|
||||||
with patch('mcp_office_tools.mixins.word.resolve_office_file_path') as mock_resolve:
|
with patch('mcwaddams.mixins.word.resolve_office_file_path') as mock_resolve:
|
||||||
with patch('mcp_office_tools.mixins.word.validate_office_file') as mock_validate:
|
with patch('mcwaddams.mixins.word.validate_office_file') as mock_validate:
|
||||||
with patch('mcp_office_tools.mixins.word.detect_format') as mock_detect:
|
with patch('mcwaddams.mixins.word.detect_format') as mock_detect:
|
||||||
mock_resolve.return_value = "/test.docx"
|
mock_resolve.return_value = "/test.docx"
|
||||||
mock_validate.return_value = {"is_valid": True, "errors": []}
|
mock_validate.return_value = {"is_valid": True, "errors": []}
|
||||||
mock_detect.return_value = {"category": "word", "extension": ".docx", "format_name": "Word"}
|
mock_detect.return_value = {"category": "word", "extension": ".docx", "format_name": "Word"}
|
||||||
@ -225,9 +225,9 @@ class TestConvertToMarkdown:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_convert_to_markdown_summary_mode(self, mixin):
|
async def test_convert_to_markdown_summary_mode(self, mixin):
|
||||||
"""Test summary_only mode functionality."""
|
"""Test summary_only mode functionality."""
|
||||||
with patch('mcp_office_tools.mixins.word.resolve_office_file_path') as mock_resolve:
|
with patch('mcwaddams.mixins.word.resolve_office_file_path') as mock_resolve:
|
||||||
with patch('mcp_office_tools.mixins.word.validate_office_file') as mock_validate:
|
with patch('mcwaddams.mixins.word.validate_office_file') as mock_validate:
|
||||||
with patch('mcp_office_tools.mixins.word.detect_format') as mock_detect:
|
with patch('mcwaddams.mixins.word.detect_format') as mock_detect:
|
||||||
mock_resolve.return_value = "/test.docx"
|
mock_resolve.return_value = "/test.docx"
|
||||||
mock_validate.return_value = {"is_valid": True, "errors": []}
|
mock_validate.return_value = {"is_valid": True, "errors": []}
|
||||||
mock_detect.return_value = {"category": "word", "extension": ".docx", "format_name": "Word"}
|
mock_detect.return_value = {"category": "word", "extension": ".docx", "format_name": "Word"}
|
||||||
@ -373,9 +373,9 @@ class TestLegacyWordSupport:
|
|||||||
return mixin
|
return mixin
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@patch('mcp_office_tools.mixins.word.resolve_office_file_path')
|
@patch('mcwaddams.mixins.word.resolve_office_file_path')
|
||||||
@patch('mcp_office_tools.mixins.word.validate_office_file')
|
@patch('mcwaddams.mixins.word.validate_office_file')
|
||||||
@patch('mcp_office_tools.mixins.word.detect_format')
|
@patch('mcwaddams.mixins.word.detect_format')
|
||||||
async def test_convert_legacy_doc_to_markdown(self, mock_detect, mock_validate, mock_resolve, mixin):
|
async def test_convert_legacy_doc_to_markdown(self, mock_detect, mock_validate, mock_resolve, mixin):
|
||||||
"""Test conversion of legacy .doc files."""
|
"""Test conversion of legacy .doc files."""
|
||||||
mock_resolve.return_value = "/test.doc"
|
mock_resolve.return_value = "/test.doc"
|
||||||
@ -425,9 +425,9 @@ class TestPageRangeFiltering:
|
|||||||
return mixin
|
return mixin
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@patch('mcp_office_tools.mixins.word.resolve_office_file_path')
|
@patch('mcwaddams.mixins.word.resolve_office_file_path')
|
||||||
@patch('mcp_office_tools.mixins.word.validate_office_file')
|
@patch('mcwaddams.mixins.word.validate_office_file')
|
||||||
@patch('mcp_office_tools.mixins.word.detect_format')
|
@patch('mcwaddams.mixins.word.detect_format')
|
||||||
async def test_page_range_filters_different_content(self, mock_detect, mock_validate, mock_resolve, mixin):
|
async def test_page_range_filters_different_content(self, mock_detect, mock_validate, mock_resolve, mixin):
|
||||||
"""Test that different page_range values return different content.
|
"""Test that different page_range values return different content.
|
||||||
|
|
||||||
|
|||||||
@ -17,8 +17,8 @@ warnings.filterwarnings("ignore", category=FutureWarning)
|
|||||||
# Add src to path
|
# Add src to path
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
|
||||||
|
|
||||||
from mcp_office_tools.mixins.excel import ExcelMixin
|
from mcwaddams.mixins.excel import ExcelMixin
|
||||||
from mcp_office_tools.mixins.word import WordMixin
|
from mcwaddams.mixins.word import WordMixin
|
||||||
|
|
||||||
|
|
||||||
# Test files - real files from user's system
|
# Test files - real files from user's system
|
||||||
|
|||||||
2
uv.lock
generated
2
uv.lock
generated
@ -1537,7 +1537,7 @@ wheels = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mcp-office-tools"
|
name = "mcwaddams"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Quick script to open the test dashboard in browser
|
# Quick script to open the test dashboard in browser
|
||||||
|
|
||||||
DASHBOARD_PATH="/home/rpm/claude/mcp-office-tools/reports/test_dashboard.html"
|
DASHBOARD_PATH="/home/rpm/claude/mcwaddams/reports/test_dashboard.html"
|
||||||
|
|
||||||
echo "📊 Opening MCP Office Tools Test Dashboard..."
|
echo "📊 Opening MCP Office Tools Test Dashboard..."
|
||||||
echo "Dashboard: $DASHBOARD_PATH"
|
echo "Dashboard: $DASHBOARD_PATH"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user