Add HTTP interface, templates, generate_from_template, unified Dockerfile
Continuous Integration / Test Suite (macos-latest, nightly) (push) Has been cancelled
Continuous Integration / Test Suite (macos-latest, stable) (push) Has been cancelled
Continuous Integration / Test Suite (ubuntu-latest, 1.70.0) (push) Has been cancelled
Continuous Integration / Test Suite (ubuntu-latest, beta) (push) Has been cancelled
Continuous Integration / Test Suite (ubuntu-latest, nightly) (push) Has been cancelled
Continuous Integration / Test Suite (ubuntu-latest, stable) (push) Has been cancelled
Continuous Integration / Test Suite (windows-latest, stable) (push) Has been cancelled
Continuous Integration / Security Audit (push) Has been cancelled
Continuous Integration / Code Coverage (push) Has been cancelled
Continuous Integration / Performance Benchmarks (push) Has been cancelled
Continuous Integration / Memory Safety Check (push) Has been cancelled
Continuous Integration / Docker Build Test (push) Has been cancelled
Continuous Integration / Release Readiness (push) Has been cancelled
Continuous Integration / Integration Tests (push) Has been cancelled
Continuous Integration / Stress Testing (push) Has been cancelled
Continuous Integration / Notify Results (push) Has been cancelled
Continuous Integration / Test Suite (macos-latest, nightly) (push) Has been cancelled
Continuous Integration / Test Suite (macos-latest, stable) (push) Has been cancelled
Continuous Integration / Test Suite (ubuntu-latest, 1.70.0) (push) Has been cancelled
Continuous Integration / Test Suite (ubuntu-latest, beta) (push) Has been cancelled
Continuous Integration / Test Suite (ubuntu-latest, nightly) (push) Has been cancelled
Continuous Integration / Test Suite (ubuntu-latest, stable) (push) Has been cancelled
Continuous Integration / Test Suite (windows-latest, stable) (push) Has been cancelled
Continuous Integration / Security Audit (push) Has been cancelled
Continuous Integration / Code Coverage (push) Has been cancelled
Continuous Integration / Performance Benchmarks (push) Has been cancelled
Continuous Integration / Memory Safety Check (push) Has been cancelled
Continuous Integration / Docker Build Test (push) Has been cancelled
Continuous Integration / Release Readiness (push) Has been cancelled
Continuous Integration / Integration Tests (push) Has been cancelled
Continuous Integration / Stress Testing (push) Has been cancelled
Continuous Integration / Notify Results (push) Has been cancelled
This commit is contained in:
@@ -95,6 +95,12 @@ once_cell = "1.20"
|
|||||||
# Command line argument parsing
|
# Command line argument parsing
|
||||||
clap = { version = "4.5", features = ["derive", "env"] }
|
clap = { version = "4.5", features = ["derive", "env"] }
|
||||||
|
|
||||||
|
# HTTP server for HTML interface
|
||||||
|
axum = { version = "0.7", features = ["ws", "json"] }
|
||||||
|
tower-http = { version = "0.5", features = ["cors"] }
|
||||||
|
hyper = { version = "1.4", features = ["full"] }
|
||||||
|
tokio-tungstenite = "0.21"
|
||||||
|
|
||||||
# Optional external tool support
|
# Optional external tool support
|
||||||
headless_chrome = { version = "1.0", optional = true }
|
headless_chrome = { version = "1.0", optional = true }
|
||||||
wkhtmltopdf = { version = "0.4", optional = true }
|
wkhtmltopdf = { version = "0.4", optional = true }
|
||||||
@@ -102,6 +108,7 @@ wkhtmltopdf = { version = "0.4", optional = true }
|
|||||||
[features]
|
[features]
|
||||||
default = ["embedded-fonts", "pure-rust-pdf"]
|
default = ["embedded-fonts", "pure-rust-pdf"]
|
||||||
runtime-server = []
|
runtime-server = []
|
||||||
|
http-server = []
|
||||||
advanced-docx = []
|
advanced-docx = []
|
||||||
embedded-fonts = []
|
embedded-fonts = []
|
||||||
pure-rust-pdf = []
|
pure-rust-pdf = []
|
||||||
|
|||||||
+338
@@ -0,0 +1,338 @@
|
|||||||
|
# docx-mcp Server - Deployment Guide
|
||||||
|
|
||||||
|
## Server Architecture
|
||||||
|
|
||||||
|
This MCP server supports:
|
||||||
|
- **stdio mode** (default): stdin/stdout for MCP clients.
|
||||||
|
- **HTTP mode**: Web interface for HTML/browser access over LAN.
|
||||||
|
- **Templates directory**: User-provided .docx templates for reuse and fill-in generation.
|
||||||
|
- **High-fidelity PDF conversion**: Via LibreOffice (included in Docker image).
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ DEPLOYMENT MODES │
|
||||||
|
├─────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ Mode 1: stdio (Local MCP Clients) │
|
||||||
|
│ ┌───────────┐ stdio ┌──────────────────┐ │
|
||||||
|
│ │ MCP │ ◄────────► │ docx-mcp │ │
|
||||||
|
│ │ Client │ │ (container) │ │
|
||||||
|
│ └───────────┘ └──────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ Mode 2: HTTP (HTML Interface - LAN) │
|
||||||
|
│ ┌───────────┐ HTTP:3000 ┌──────────────────┐ │
|
||||||
|
│ │ Browser │ ◄────────────►│ docx-mcp │ │
|
||||||
|
│ │ (HTML) │ │ (container) │ │
|
||||||
|
│ └───────────┘ └──────────────────┘ │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker Image
|
||||||
|
|
||||||
|
There is now a single, unified Dockerfile that includes:
|
||||||
|
- HTTP server (HTML interface)
|
||||||
|
- stdio MCP transport
|
||||||
|
- LibreOffice (high-fidelity PDF conversion)
|
||||||
|
- Templates directory support
|
||||||
|
- Sandboxed, non-root configuration
|
||||||
|
|
||||||
|
Build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t docx-mcp:full .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
### HTTP Mode (HTML Interface - LAN)
|
||||||
|
|
||||||
|
Run the HTTP server with templates and output directories mounted:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm \
|
||||||
|
--name docx-mcp-http \
|
||||||
|
-p 3000:3000 \
|
||||||
|
-v /host/path/templates:/templates:ro \
|
||||||
|
-v /host/path/output:/out \
|
||||||
|
-e DOCX_MCP_HTTP=true \
|
||||||
|
-e DOCX_MCP_HTTP_ADDRESS=0.0.0.0:3000 \
|
||||||
|
-e DOCX_MCP_TEMPLATES_DIR=/templates \
|
||||||
|
-e DOCX_MCP_MAX_SIZE=104857600 \
|
||||||
|
-e DOCX_MCP_MAX_DOCS=30 \
|
||||||
|
--memory 1g \
|
||||||
|
--cpus 1.5 \
|
||||||
|
docx-mcp:full
|
||||||
|
```
|
||||||
|
|
||||||
|
Access:
|
||||||
|
- HTML Interface: http://your-server-ip:3000
|
||||||
|
- API: http://your-server-ip:3000/api/tools
|
||||||
|
- WebSocket: ws://your-server-ip:3000/ws
|
||||||
|
|
||||||
|
### stdio Mode (for MCP Clients)
|
||||||
|
|
||||||
|
Useful when launched by an MCP client (e.g., Claude Desktop, Cursor).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm \
|
||||||
|
--name docx-mcp-stdio \
|
||||||
|
-v /host/path/templates:/templates:ro \
|
||||||
|
-v /host/path/output:/out \
|
||||||
|
-e DOCX_MCP_TEMPLATES_DIR=/templates \
|
||||||
|
-e DOCX_MCP_MAX_SIZE=104857600 \
|
||||||
|
-e DOCX_MCP_MAX_DOCS=30 \
|
||||||
|
--memory 1g \
|
||||||
|
--cpus 1.5 \
|
||||||
|
docx-mcp:full
|
||||||
|
```
|
||||||
|
|
||||||
|
In MCP client config, point "command" to "docker run" with these flags.
|
||||||
|
|
||||||
|
## Server Configuration
|
||||||
|
|
||||||
|
### Command Line Arguments
|
||||||
|
|
||||||
|
| Argument | Environment Variable | Description |
|
||||||
|
|----------|---------------------|-------------|
|
||||||
|
| `--http-mode` | `DOCX_MCP_HTTP=true` | Enable HTTP server mode |
|
||||||
|
| `--http-address` | `DOCX_MCP_HTTP_ADDRESS` | HTTP server address (default: 0.0.0.0:3000) |
|
||||||
|
| `--templates-dir` | `DOCX_MCP_TEMPLATES_DIR` | Directory with template .docx files (default: /templates) |
|
||||||
|
| `--readonly` | `DOCX_MCP_READONLY=true` | Enable readonly mode |
|
||||||
|
| `--sandbox` | `DOCX_MCP_SANDBOX=true` | Enable sandbox mode |
|
||||||
|
| `--no-external-tools` | `DOCX_MCP_NO_EXTERNAL_TOOLS=true` | Disable external tools (e.g., LibreOffice) |
|
||||||
|
| `--no-network` | `DOCX_MCP_NO_NETWORK=true` | Disable network operations |
|
||||||
|
| `--max-size` | `DOCX_MCP_MAX_SIZE` | Max document size in bytes |
|
||||||
|
| `--max-docs` | `DOCX_MCP_MAX_DOCS` | Max concurrent open documents |
|
||||||
|
| `--whitelist` | `DOCX_MCP_WHITELIST` | Allowed tools (comma-separated) |
|
||||||
|
| `--blacklist` | `DOCX_MCP_BLACKLIST` | Blocked tools (comma-separated) |
|
||||||
|
|
||||||
|
### Example Configurations
|
||||||
|
|
||||||
|
- HTTP mode with templates:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm \
|
||||||
|
-p 3000:3000 \
|
||||||
|
-v /host/path/templates:/templates:ro \
|
||||||
|
-e DOCX_MCP_HTTP=true \
|
||||||
|
-e DOCX_MCP_TEMPLATES_DIR=/templates \
|
||||||
|
docx-mcp:full
|
||||||
|
```
|
||||||
|
|
||||||
|
- Readonly HTTP mode (limited tools):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm \
|
||||||
|
-p 3000:3000 \
|
||||||
|
-e DOCX_MCP_HTTP=true \
|
||||||
|
-e DOCX_MCP_READONLY=true \
|
||||||
|
-e DOCX_MCP_WHITELIST="list_templates,open_template,extract_text,get_metadata,search_text" \
|
||||||
|
docx-mcp:full
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### HTML Interface
|
||||||
|
- GET / — Web interface (tool browser + templates panel)
|
||||||
|
|
||||||
|
### REST API
|
||||||
|
- GET /api/tools — List available tools
|
||||||
|
- POST /api/call — Call a tool
|
||||||
|
|
||||||
|
### WebSocket
|
||||||
|
- WS /ws — Real-time communication
|
||||||
|
|
||||||
|
### API Examples
|
||||||
|
|
||||||
|
- List tools:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:3000/api/tools
|
||||||
|
```
|
||||||
|
|
||||||
|
- Call a tool:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3000/api/call \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"name": "create_document",
|
||||||
|
"arguments": {}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
- List templates:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3000/api/call \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"name": "list_templates",
|
||||||
|
"arguments": {}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
- Open a template:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3000/api/call \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"name": "open_template",
|
||||||
|
"arguments": { "name": "nda_template.docx" }
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
- Generate from template with fill-in fields:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3000/api/call \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"name": "generate_from_template",
|
||||||
|
"arguments": {
|
||||||
|
"template_name": "nda_template.docx",
|
||||||
|
"output_path": "/out/nda_acme.docx",
|
||||||
|
"fields": {
|
||||||
|
"CLIENT_NAME": "Acme Corp",
|
||||||
|
"EFFECTIVE_DATE": "2025-11-09"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker Compose (Production)
|
||||||
|
|
||||||
|
Example with HTTP mode, templates, and output volumes:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
docx-mcp:
|
||||||
|
image: docx-mcp:full
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
read_only: true
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
|
tmpfs:
|
||||||
|
- /tmp/docx-mcp:rw,noexec,nosuid,size=200m
|
||||||
|
volumes:
|
||||||
|
- ./templates:/templates:ro
|
||||||
|
- ./output:/out
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
environment:
|
||||||
|
- RUST_LOG=info
|
||||||
|
- DOCX_MCP_HTTP=true
|
||||||
|
- DOCX_MCP_HTTP_ADDRESS=0.0.0.0:3000
|
||||||
|
- DOCX_MCP_TEMPLATES_DIR=/templates
|
||||||
|
- DOCX_MCP_MAX_SIZE=104857600
|
||||||
|
- DOCX_MCP_MAX_DOCS=30
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 1G
|
||||||
|
cpus: '1.5'
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "/usr/local/bin/docx-mcp", "--version"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Configuration
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `DOCX_MCP_HTTP` | `false` | Enable HTTP mode |
|
||||||
|
| `DOCX_MCP_HTTP_ADDRESS` | `0.0.0.0:3000` | HTTP server address |
|
||||||
|
| `DOCX_MCP_TEMPLATES_DIR` | `/templates` | Templates directory |
|
||||||
|
| `DOCX_MCP_READONLY` | `false` | Restrict to read-only operations |
|
||||||
|
| `DOCX_MCP_SANDBOX` | `true` | Restrict file operations to temp |
|
||||||
|
| `DOCX_MCP_NO_EXTERNAL_TOOLS` | `true` | Disable external tools |
|
||||||
|
| `DOCX_MCP_NO_NETWORK` | `true` | Disable network access |
|
||||||
|
| `DOCX_MCP_MAX_SIZE` | `104857600` | Max document size (bytes) |
|
||||||
|
| `DOCX_MCP_MAX_DOCS` | `30` | Max concurrent documents |
|
||||||
|
| `DOCX_MCP_WHITELIST` | - | Allowed tools (comma-separated) |
|
||||||
|
| `DOCX_MCP_BLACKLIST` | - | Blocked tools (comma-separated) |
|
||||||
|
|
||||||
|
### Security Profiles
|
||||||
|
|
||||||
|
- Readonly HTTP mode:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm \
|
||||||
|
-p 3000:3000 \
|
||||||
|
-e DOCX_MCP_HTTP=true \
|
||||||
|
-e DOCX_MCP_READONLY=true \
|
||||||
|
-e DOCX_MCP_WHITELIST="list_templates,open_template,extract_text,get_metadata,search_text" \
|
||||||
|
docx-mcp:full
|
||||||
|
```
|
||||||
|
|
||||||
|
- Maximum security:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm \
|
||||||
|
-p 3000:3000 \
|
||||||
|
--read-only \
|
||||||
|
--cap-drop ALL \
|
||||||
|
--tmpfs /tmp/docx-mcp \
|
||||||
|
-e DOCX_MCP_HTTP=true \
|
||||||
|
-e DOCX_MCP_READONLY=true \
|
||||||
|
-e DOCX_MCP_SANDBOX=true \
|
||||||
|
-e DOCX_MCP_NO_EXTERNAL_TOOLS=true \
|
||||||
|
-e DOCX_MCP_NO_NETWORK=true \
|
||||||
|
docx-mcp:full
|
||||||
|
```
|
||||||
|
|
||||||
|
## Monitoring
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View logs
|
||||||
|
docker logs -f docx-mcp-http
|
||||||
|
|
||||||
|
# Check resource usage
|
||||||
|
docker stats docx-mcp-http
|
||||||
|
|
||||||
|
# Verify security
|
||||||
|
docker inspect --format='{{.HostConfig.ReadOnly}}' docx-mcp-http # Should be true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. Port already in use:
|
||||||
|
- Use a different port:
|
||||||
|
- -p 8080:8080 -e DOCX_MCP_HTTP_ADDRESS=0.0.0.0:8080
|
||||||
|
|
||||||
|
2. Permission denied on temp directory:
|
||||||
|
- Ensure temp directory is writable:
|
||||||
|
- --tmpfs /tmp/docx-mcp:rw
|
||||||
|
|
||||||
|
3. Out of memory:
|
||||||
|
- Increase memory:
|
||||||
|
- --memory 2g
|
||||||
|
|
||||||
|
4. CORS issues in browser:
|
||||||
|
- CORS is enabled for all origins on LAN by default.
|
||||||
|
- For production, restrict to specific origins as needed.
|
||||||
|
|
||||||
|
## API Key
|
||||||
|
|
||||||
|
No API key is required. Security relies on:
|
||||||
|
- OS-level access controls
|
||||||
|
- Container isolation
|
||||||
|
- Built-in command security (whitelist/blacklist)
|
||||||
|
|
||||||
|
For LAN deployments, rely on:
|
||||||
|
- Network-level access controls
|
||||||
|
- Firewall rules
|
||||||
|
- Application-level authentication at the bridge
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
# docx-mcp Server - Deployment Quick Reference
|
||||||
|
|
||||||
|
## Key Facts
|
||||||
|
|
||||||
|
| Item | Value |
|
||||||
|
|------|-------|
|
||||||
|
| **Transport Method** | stdio (stdin/stdout) |
|
||||||
|
| **Network Port** | Not required for local use |
|
||||||
|
| **API Key** | Not required |
|
||||||
|
| **Authentication** | OS-level + container security |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Port Requirements
|
||||||
|
|
||||||
|
### Local Deployment (Recommended)
|
||||||
|
**No port required** - the server communicates via stdin/stdout directly.
|
||||||
|
|
||||||
|
### Remote Deployment (Optional)
|
||||||
|
If remote access is needed, wrap with a stdio-to-network bridge:
|
||||||
|
|
||||||
|
| Bridge Type | Port | Protocol |
|
||||||
|
|-------------|------|----------|
|
||||||
|
| WebSocket | 8080 | ws:// |
|
||||||
|
| TCP | 8080 | tcp:// |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Build
|
||||||
|
```bash
|
||||||
|
# Minimal (recommended)
|
||||||
|
docker build -f Dockerfile.sandboxed -t docx-mcp:sandboxed .
|
||||||
|
|
||||||
|
# With LibreOffice (better PDF conversion)
|
||||||
|
docker build -f Dockerfile.libreoffice -t docx-mcp:libreoffice .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run (Local)
|
||||||
|
```bash
|
||||||
|
docker run --rm \
|
||||||
|
--name docx-mcp \
|
||||||
|
--read-only \
|
||||||
|
--cap-drop ALL \
|
||||||
|
--tmpfs /tmp/docx-mcp \
|
||||||
|
--memory 512m \
|
||||||
|
docx-mcp:sandboxed
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run (Remote via Docker Compose)
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MCP Client Configuration
|
||||||
|
|
||||||
|
### Claude Desktop
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"docx": {
|
||||||
|
"command": "docker",
|
||||||
|
"args": [
|
||||||
|
"run", "--rm", "--read-only", "--cap-drop ALL",
|
||||||
|
"--tmpfs /tmp/docx-mcp", "--memory 512m",
|
||||||
|
"docx-mcp:sandboxed"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cursor
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcp": {
|
||||||
|
"servers": {
|
||||||
|
"docx": {
|
||||||
|
"command": "docker",
|
||||||
|
"args": [
|
||||||
|
"run", "--rm", "--read-only", "--cap-drop ALL",
|
||||||
|
"--tmpfs /tmp/docx-mcp", "--memory 512m",
|
||||||
|
"docx-mcp:sandboxed"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Profiles
|
||||||
|
|
||||||
|
### Readonly Mode
|
||||||
|
```bash
|
||||||
|
docker run --rm \
|
||||||
|
-e DOCX_MCP_READONLY=true \
|
||||||
|
-e DOCX_MCP_WHITELIST="open_document,extract_text,get_metadata,search_text" \
|
||||||
|
docx-mcp:sandboxed
|
||||||
|
```
|
||||||
|
|
||||||
|
### Maximum Security
|
||||||
|
```bash
|
||||||
|
docker run --rm \
|
||||||
|
--read-only \
|
||||||
|
--cap-drop ALL \
|
||||||
|
--network none \
|
||||||
|
--tmpfs /tmp/docx-mcp \
|
||||||
|
-e DOCX_MCP_READONLY=true \
|
||||||
|
-e DOCX_MCP_SANDBOX=true \
|
||||||
|
-e DOCX_MCP_NO_EXTERNAL_TOOLS=true \
|
||||||
|
-e DOCX_MCP_NO_NETWORK=true \
|
||||||
|
docx-mcp:sandboxed
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `DOCX_MCP_READONLY` | `false` | Restrict to read-only operations |
|
||||||
|
| `DOCX_MCP_SANDBOX` | `true` | Restrict file operations to temp |
|
||||||
|
| `DOCX_MCP_NO_EXTERNAL_TOOLS` | `true` | Disable LibreOffice etc. |
|
||||||
|
| `DOCX_MCP_NO_NETWORK` | `true` | Disable network access |
|
||||||
|
| `DOCX_MCP_MAX_SIZE` | `52428800` | Max document size (bytes) |
|
||||||
|
| `DOCX_MCP_MAX_DOCS` | `20` | Max concurrent documents |
|
||||||
|
| `DOCX_MCP_WHITELIST` | - | Allowed tools (comma-separated) |
|
||||||
|
| `DOCX_MCP_BLACKLIST` | - | Blocked tools (comma-separated) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files Created
|
||||||
|
|
||||||
|
| File | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `Dockerfile.sandboxed` | Minimal security-focused image |
|
||||||
|
| `Dockerfile.libreoffice` | Full features with LibreOffice |
|
||||||
|
| `docker-compose.yml` | Production deployment config |
|
||||||
|
| `DEPLOYMENT.md` | Comprehensive deployment guide |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
- **Port Required:** No (for local) / 8080 (for remote with bridge)
|
||||||
|
- **API Key:** No
|
||||||
|
- **Authentication:** Container isolation + OS controls
|
||||||
|
- **Recommended:** Local stdio transport with security features enabled
|
||||||
+50
-34
@@ -1,8 +1,17 @@
|
|||||||
# Multi-stage Docker build for docx-mcp
|
# Unified Dockerfile for docx-mcp
|
||||||
FROM rust:1.75-slim as builder
|
# Features:
|
||||||
|
# - HTTP mode (HTML interface) + stdio mode
|
||||||
|
# - LibreOffice for high-fidelity PDF conversion
|
||||||
|
# - Templates directory support
|
||||||
|
# - Sandboxed, non-root, read-only filesystem where possible
|
||||||
|
|
||||||
# Install system dependencies for building
|
# ============================================================
|
||||||
RUN apt-get update && apt-get install -y \
|
# Build Stage
|
||||||
|
# ============================================================
|
||||||
|
FROM rust:1.80-slim AS builder
|
||||||
|
|
||||||
|
# Install build dependencies
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
pkg-config \
|
pkg-config \
|
||||||
libssl-dev \
|
libssl-dev \
|
||||||
libfontconfig1-dev \
|
libfontconfig1-dev \
|
||||||
@@ -12,26 +21,26 @@ RUN apt-get update && apt-get install -y \
|
|||||||
build-essential \
|
build-essential \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Set working directory
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy manifests
|
# Copy manifests and source
|
||||||
COPY Cargo.toml Cargo.lock ./
|
COPY Cargo.toml Cargo.lock build.rs ./
|
||||||
COPY build.rs ./
|
|
||||||
|
|
||||||
# Copy source code
|
|
||||||
COPY src/ ./src/
|
COPY src/ ./src/
|
||||||
COPY benches/ ./benches/
|
COPY assets/ ./assets/
|
||||||
COPY tests/ ./tests/
|
|
||||||
|
|
||||||
# Build the application
|
# Build with all key features enabled:
|
||||||
RUN cargo build --release --all-features
|
# - runtime-server: stdio MCP transport
|
||||||
|
# - http-server: HTTP + HTML interface
|
||||||
|
# - advanced-docx: advanced document operations
|
||||||
|
RUN cargo build --release --features "runtime-server http-server advanced-docx"
|
||||||
|
|
||||||
# Runtime stage
|
# ============================================================
|
||||||
FROM debian:bookworm-slim
|
# Runtime Stage
|
||||||
|
# ============================================================
|
||||||
|
FROM debian:bookworm-slim AS runtime
|
||||||
|
|
||||||
# Install runtime dependencies
|
# Install runtime dependencies (including LibreOffice for better PDF conversion)
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
libssl3 \
|
libssl3 \
|
||||||
libfontconfig1 \
|
libfontconfig1 \
|
||||||
libfreetype6 \
|
libfreetype6 \
|
||||||
@@ -45,33 +54,40 @@ RUN apt-get update && apt-get install -y \
|
|||||||
# Create non-root user
|
# Create non-root user
|
||||||
RUN groupadd -r docxmcp && useradd -r -g docxmcp -s /bin/bash -d /app docxmcp
|
RUN groupadd -r docxmcp && useradd -r -g docxmcp -s /bin/bash -d /app docxmcp
|
||||||
|
|
||||||
# Create app directory and set ownership
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN chown -R docxmcp:docxmcp /app
|
RUN chown -R docxmcp:docxmcp /app
|
||||||
|
|
||||||
# Copy the built binary from builder stage
|
# Copy binary from builder
|
||||||
COPY --from=builder /app/target/release/docx-mcp /usr/local/bin/docx-mcp
|
COPY --from=builder /app/target/release/docx-mcp /usr/local/bin/docx-mcp
|
||||||
RUN chmod +x /usr/local/bin/docx-mcp
|
RUN chmod +x /usr/local/bin/docx-mcp
|
||||||
|
|
||||||
# Copy additional files if needed
|
# Create working directories
|
||||||
COPY README.md LICENSE ./
|
RUN mkdir -p /tmp/docx-mcp /templates /out && \
|
||||||
|
chown -R docxmcp:docxmcp /tmp/docx-mcp /templates /out
|
||||||
|
|
||||||
# Switch to non-root user
|
# Switch to non-root user
|
||||||
USER docxmcp
|
USER docxmcp
|
||||||
|
|
||||||
# Create temp directory for document processing
|
# Expose HTTP port (used when running in HTTP mode)
|
||||||
RUN mkdir -p /tmp/docx-mcp && chmod 755 /tmp/docx-mcp
|
EXPOSE 3000
|
||||||
|
|
||||||
# Expose default MCP port (though MCP typically uses stdin/stdout)
|
# Health check (checks binary is present and executable)
|
||||||
EXPOSE 8080
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||||
|
CMD /usr/local/bin/docx-mcp --version
|
||||||
|
|
||||||
# Health check
|
# Default environment:
|
||||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
# - HTTP disabled by default (use stdio mode).
|
||||||
CMD /usr/local/bin/docx-mcp --version || exit 1
|
# - Enable via DOCX_MCP_HTTP=true or --http-mode.
|
||||||
|
|
||||||
# Set environment variables
|
|
||||||
ENV RUST_LOG=info
|
ENV RUST_LOG=info
|
||||||
ENV DOCX_MCP_TEMP_DIR=/tmp/docx-mcp
|
ENV DOCX_MCP_TEMP=/tmp/docx-mcp
|
||||||
|
ENV DOCX_MCP_HTTP=false
|
||||||
|
ENV DOCX_MCP_HTTP_ADDRESS=0.0.0.0:3000
|
||||||
|
ENV DOCX_MCP_TEMPLATES_DIR=/templates
|
||||||
|
ENV DOCX_MCP_MAX_SIZE=104857600
|
||||||
|
ENV DOCX_MCP_MAX_DOCS=30
|
||||||
|
|
||||||
# Default command
|
ENTRYPOINT ["/usr/local/bin/docx-mcp"]
|
||||||
CMD ["/usr/local/bin/docx-mcp"]
|
|
||||||
|
# Default: stdio mode (for MCP clients).
|
||||||
|
# To run in HTTP mode, override CMD or set DOCX_MCP_HTTP=true.
|
||||||
|
CMD []
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ A comprehensive Model Context Protocol (MCP) server for Microsoft Word DOCX file
|
|||||||
## 📖 Table of Contents
|
## 📖 Table of Contents
|
||||||
|
|
||||||
- [Quick Start](#-quick-start)
|
- [Quick Start](#-quick-start)
|
||||||
|
- [Transport Modes](#-transport-modes)
|
||||||
- [AI Tool Integration](#-ai-tool-integration)
|
- [AI Tool Integration](#-ai-tool-integration)
|
||||||
- [Claude Desktop](#claude-desktop)
|
- [Claude Desktop](#claude-desktop)
|
||||||
- [Cursor](#cursor)
|
- [Cursor](#cursor)
|
||||||
@@ -58,6 +59,7 @@ The server will automatically use external tools if available for enhanced quali
|
|||||||
The server includes comprehensive security features for enterprise and restricted environments:
|
The server includes comprehensive security features for enterprise and restricted environments:
|
||||||
|
|
||||||
### Readonly Mode
|
### Readonly Mode
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Enable readonly mode - only allows document viewing and analysis
|
# Enable readonly mode - only allows document viewing and analysis
|
||||||
|
|
||||||
@@ -77,6 +79,7 @@ In readonly mode, only these operations are allowed:
|
|||||||
- Get document metadata and statistics
|
- Get document metadata and statistics
|
||||||
|
|
||||||
### Command Filtering
|
### Command Filtering
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Whitelist specific commands only
|
# Whitelist specific commands only
|
||||||
|
|
||||||
@@ -96,6 +99,7 @@ export DOCX_MCP_BLACKLIST="save_document,convert_to_pdf,merge_documents"
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Sandbox Mode
|
### Sandbox Mode
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Restrict all file operations to temp directory only
|
# Restrict all file operations to temp directory only
|
||||||
|
|
||||||
@@ -108,6 +112,7 @@ export DOCX_MCP_SANDBOX=true
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Resource Limits
|
### Resource Limits
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Set maximum document size (100MB default)
|
# Set maximum document size (100MB default)
|
||||||
|
|
||||||
@@ -126,6 +131,37 @@ export DOCX_MCP_NO_NETWORK=true
|
|||||||
--no-network
|
--no-network
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 🌐 Transport Modes
|
||||||
|
|
||||||
|
The server supports two transport modes:
|
||||||
|
|
||||||
|
- **stdio (default)**:
|
||||||
|
- Communicates over stdin/stdout.
|
||||||
|
- Ideal for MCP clients (Claude Desktop, Cursor, etc.).
|
||||||
|
|
||||||
|
- **HTTP (for HTML/browser access over LAN)**:
|
||||||
|
- Provides:
|
||||||
|
- A web interface for browsing and using tools.
|
||||||
|
- A REST API for programmatic access.
|
||||||
|
- A WebSocket endpoint for real-time communication.
|
||||||
|
|
||||||
|
To enable HTTP mode:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using command line
|
||||||
|
./target/release/docx-mcp --http-mode --http-address 0.0.0.0:3000
|
||||||
|
|
||||||
|
# Using environment variables
|
||||||
|
export DOCX_MCP_HTTP=true
|
||||||
|
export DOCX_MCP_HTTP_ADDRESS=0.0.0.0:3000
|
||||||
|
./target/release/docx-mcp
|
||||||
|
```
|
||||||
|
|
||||||
|
Access:
|
||||||
|
- HTML Interface: http://your-server-ip:3000
|
||||||
|
- API: http://your-server-ip:3000/api/tools
|
||||||
|
- WebSocket: ws://your-server-ip:3000/ws
|
||||||
|
|
||||||
## 🤖 AI Tool Integration
|
## 🤖 AI Tool Integration
|
||||||
|
|
||||||
### Claude Desktop
|
### Claude Desktop
|
||||||
@@ -150,6 +186,7 @@ Add to your Claude Desktop configuration file:
|
|||||||
```
|
```
|
||||||
|
|
||||||
**With Security Options (using command-line arguments):**
|
**With Security Options (using command-line arguments):**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
@@ -165,6 +202,7 @@ Add to your Claude Desktop configuration file:
|
|||||||
```
|
```
|
||||||
|
|
||||||
**With Security Options (using environment variables):**
|
**With Security Options (using environment variables):**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
@@ -193,6 +231,7 @@ After adding, restart Claude Desktop. You can then ask Claude to:
|
|||||||
Add to your Cursor settings (`~/.cursor/config.json` or through Settings UI):
|
Add to your Cursor settings (`~/.cursor/config.json` or through Settings UI):
|
||||||
|
|
||||||
**Basic Configuration:**
|
**Basic Configuration:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"mcp": {
|
"mcp": {
|
||||||
@@ -210,6 +249,7 @@ Add to your Cursor settings (`~/.cursor/config.json` or through Settings UI):
|
|||||||
```
|
```
|
||||||
|
|
||||||
**With Security Options (using command-line arguments):**
|
**With Security Options (using command-line arguments):**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"mcp": {
|
"mcp": {
|
||||||
@@ -227,6 +267,7 @@ Add to your Cursor settings (`~/.cursor/config.json` or through Settings UI):
|
|||||||
```
|
```
|
||||||
|
|
||||||
**With Security Options (using environment variables):**
|
**With Security Options (using environment variables):**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"mcp": {
|
"mcp": {
|
||||||
@@ -250,6 +291,7 @@ Add to your Cursor settings (`~/.cursor/config.json` or through Settings UI):
|
|||||||
Add to your Windsurf configuration (`~/.windsurf/config.json`):
|
Add to your Windsurf configuration (`~/.windsurf/config.json`):
|
||||||
|
|
||||||
**Basic Configuration:**
|
**Basic Configuration:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"mcp": {
|
"mcp": {
|
||||||
@@ -267,6 +309,7 @@ Add to your Windsurf configuration (`~/.windsurf/config.json`):
|
|||||||
```
|
```
|
||||||
|
|
||||||
**With Security Options (using arguments):**
|
**With Security Options (using arguments):**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"mcp": {
|
"mcp": {
|
||||||
@@ -288,6 +331,7 @@ Add to your Windsurf configuration (`~/.windsurf/config.json`):
|
|||||||
Add to your Continue configuration (`~/.continue/config.json`):
|
Add to your Continue configuration (`~/.continue/config.json`):
|
||||||
|
|
||||||
**Basic Configuration:**
|
**Basic Configuration:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"models": [
|
"models": [
|
||||||
@@ -306,6 +350,7 @@ Add to your Continue configuration (`~/.continue/config.json`):
|
|||||||
```
|
```
|
||||||
|
|
||||||
**With Security Options:**
|
**With Security Options:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"models": [
|
"models": [
|
||||||
@@ -328,6 +373,7 @@ Add to your Continue configuration (`~/.continue/config.json`):
|
|||||||
If using the MCP extension for VS Code, add to your workspace settings (`.vscode/settings.json`):
|
If using the MCP extension for VS Code, add to your workspace settings (`.vscode/settings.json`):
|
||||||
|
|
||||||
**Basic Configuration:**
|
**Basic Configuration:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"mcp.servers": {
|
"mcp.servers": {
|
||||||
@@ -343,6 +389,7 @@ If using the MCP extension for VS Code, add to your workspace settings (`.vscode
|
|||||||
```
|
```
|
||||||
|
|
||||||
**With Security Options:**
|
**With Security Options:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"mcp.servers": {
|
"mcp.servers": {
|
||||||
@@ -369,6 +416,9 @@ docx-mcp --help
|
|||||||
|
|
||||||
| Argument | Environment Variable | Description | Example |
|
| Argument | Environment Variable | Description | Example |
|
||||||
|----------|---------------------|-------------|---------|
|
|----------|---------------------|-------------|---------|
|
||||||
|
| `--http-mode` | `DOCX_MCP_HTTP=true` | Enable HTTP server mode (HTML interface) | `--http-mode` |
|
||||||
|
| `--http-address` | `DOCX_MCP_HTTP_ADDRESS` | HTTP server address (default: 0.0.0.0:3000) | `--http-address 0.0.0.0:3000` |
|
||||||
|
| `--templates-dir` | `DOCX_MCP_TEMPLATES_DIR` | Directory with template .docx files (default: /templates) | `--templates-dir /path/to/templates` |
|
||||||
| `--readonly` | `DOCX_MCP_READONLY=true` | Enable readonly mode - only viewing operations | `--readonly` |
|
| `--readonly` | `DOCX_MCP_READONLY=true` | Enable readonly mode - only viewing operations | `--readonly` |
|
||||||
| `--whitelist <COMMANDS>` | `DOCX_MCP_WHITELIST` | Comma-separated list of allowed commands | `--whitelist open_document,extract_text` |
|
| `--whitelist <COMMANDS>` | `DOCX_MCP_WHITELIST` | Comma-separated list of allowed commands | `--whitelist open_document,extract_text` |
|
||||||
| `--blacklist <COMMANDS>` | `DOCX_MCP_BLACKLIST` | Comma-separated list of forbidden commands | `--blacklist save_document,convert_to_pdf` |
|
| `--blacklist <COMMANDS>` | `DOCX_MCP_BLACKLIST` | Comma-separated list of forbidden commands | `--blacklist save_document,convert_to_pdf` |
|
||||||
@@ -383,9 +433,12 @@ docx-mcp --help
|
|||||||
### Example Usage
|
### Example Usage
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Basic usage
|
# Basic usage (stdio mode)
|
||||||
./target/release/docx-mcp
|
./target/release/docx-mcp
|
||||||
|
|
||||||
|
# HTTP mode for HTML interface
|
||||||
|
./target/release/docx-mcp --http-mode --http-address 0.0.0.0:3000
|
||||||
|
|
||||||
# Readonly mode with size limit
|
# Readonly mode with size limit
|
||||||
./target/release/docx-mcp --readonly --max-size 10485760
|
./target/release/docx-mcp --readonly --max-size 10485760
|
||||||
|
|
||||||
@@ -428,6 +481,37 @@ docx-mcp --help
|
|||||||
- **Multiple Documents**: Handle multiple documents simultaneously
|
- **Multiple Documents**: Handle multiple documents simultaneously
|
||||||
- **Temp File Management**: Automatic cleanup of temporary files
|
- **Temp File Management**: Automatic cleanup of temporary files
|
||||||
|
|
||||||
|
### Templates and Fill-in Generation
|
||||||
|
|
||||||
|
The server supports a templates directory for reusable document templates:
|
||||||
|
|
||||||
|
- **Templates directory**:
|
||||||
|
- Configurable via `--templates-dir` or `DOCX_MCP_TEMPLATES_DIR`.
|
||||||
|
- Intended to be mounted in Docker (e.g., `/templates`).
|
||||||
|
- **Template tools**:
|
||||||
|
- `list_templates`: Lists all `.docx` templates available.
|
||||||
|
- `open_template`: Opens a template by name as a working document.
|
||||||
|
- `generate_from_template`: Generates a new document from a template with fill-in fields.
|
||||||
|
- **Fill-in fields**:
|
||||||
|
- Use placeholders like `{{CLIENT_NAME}}` in your template.
|
||||||
|
- Provide field values as key-value pairs when calling `generate_from_template`.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "generate_from_template",
|
||||||
|
"arguments": {
|
||||||
|
"template_name": "nda_template.docx",
|
||||||
|
"output_path": "/out/nda_acme.docx",
|
||||||
|
"fields": {
|
||||||
|
"CLIENT_NAME": "Acme Corp",
|
||||||
|
"EFFECTIVE_DATE": "2025-11-09"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Professional Templates
|
### Professional Templates
|
||||||
- **Business Letters**: Professional correspondence with proper formatting
|
- **Business Letters**: Professional correspondence with proper formatting
|
||||||
- **Resumes**: Modern resume layouts with sections for experience, education, skills
|
- **Resumes**: Modern resume layouts with sections for experience, education, skills
|
||||||
@@ -664,6 +748,46 @@ Saves the document to a specified path.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Template Tools
|
||||||
|
|
||||||
|
#### `list_templates`
|
||||||
|
Lists all available templates in the configured templates directory.
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tool": "list_templates",
|
||||||
|
"arguments": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `open_template`
|
||||||
|
Opens a template document by name from the templates directory.
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tool": "open_template",
|
||||||
|
"arguments": {
|
||||||
|
"name": "nda_template.docx"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `generate_from_template`
|
||||||
|
Generates a new document from a template by filling placeholders like `{{FIELD_NAME}}` with provided values.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tool": "generate_from_template",
|
||||||
|
"arguments": {
|
||||||
|
"template_name": "nda_template.docx",
|
||||||
|
"output_path": "/out/nda_acme.docx",
|
||||||
|
"fields": {
|
||||||
|
"CLIENT_NAME": "Acme Corp",
|
||||||
|
"EFFECTIVE_DATE": "2025-11-09"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Content Addition
|
### Content Addition
|
||||||
|
|
||||||
#### `add_paragraph`
|
#### `add_paragraph`
|
||||||
@@ -784,6 +908,7 @@ Finds and replaces text in the document.
|
|||||||
## Example Workflows
|
## Example Workflows
|
||||||
|
|
||||||
### Creating a Report
|
### Creating a Report
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// 1. Create a new document
|
// 1. Create a new document
|
||||||
const doc = await mcp.call("create_document", {});
|
const doc = await mcp.call("create_document", {});
|
||||||
@@ -820,6 +945,7 @@ await mcp.call("convert_to_pdf", {
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Batch Processing Documents
|
### Batch Processing Documents
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// Open and convert multiple documents
|
// Open and convert multiple documents
|
||||||
const documents = ["doc1.docx", "doc2.docx", "doc3.docx"];
|
const documents = ["doc1.docx", "doc2.docx", "doc3.docx"];
|
||||||
@@ -850,6 +976,23 @@ for (const docPath of documents) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Using Templates
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 1. List available templates
|
||||||
|
const templates = await mcp.call("list_templates", {});
|
||||||
|
|
||||||
|
// 2. Generate a new document from a template with fill-in fields
|
||||||
|
const result = await mcp.call("generate_from_template", {
|
||||||
|
template_name: "nda_template.docx",
|
||||||
|
output_path: "/out/nda_acme.docx",
|
||||||
|
fields: {
|
||||||
|
"CLIENT_NAME": "Acme Corp",
|
||||||
|
"EFFECTIVE_DATE": "2025-11-09"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
The server is built with a modular architecture:
|
The server is built with a modular architecture:
|
||||||
@@ -858,20 +1001,24 @@ The server is built with a modular architecture:
|
|||||||
- **`docx_handler.rs`**: Core DOCX manipulation logic
|
- **`docx_handler.rs`**: Core DOCX manipulation logic
|
||||||
- **`converter.rs`**: PDF and image conversion functionality
|
- **`converter.rs`**: PDF and image conversion functionality
|
||||||
- **`docx_tools.rs`**: MCP tool definitions and handlers
|
- **`docx_tools.rs`**: MCP tool definitions and handlers
|
||||||
|
- **`http_server.rs`**: HTTP server and HTML interface for LAN access
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
### Building from Source
|
### Building from Source
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo build
|
cargo build
|
||||||
```
|
```
|
||||||
|
|
||||||
### Running Tests
|
### Running Tests
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo test
|
cargo test
|
||||||
```
|
```
|
||||||
|
|
||||||
### Debug Mode
|
### Debug Mode
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
RUST_LOG=debug cargo run
|
RUST_LOG=debug cargo run
|
||||||
```
|
```
|
||||||
@@ -1045,4 +1192,4 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|||||||
- Built with the official [MCP Rust SDK](https://github.com/modelcontextprotocol/rust-sdk)
|
- Built with the official [MCP Rust SDK](https://github.com/modelcontextprotocol/rust-sdk)
|
||||||
- Uses [docx-rs](https://github.com/bokuweb/docx-rs) for DOCX manipulation
|
- Uses [docx-rs](https://github.com/bokuweb/docx-rs) for DOCX manipulation
|
||||||
- PDF generation with [printpdf](https://github.com/fschutt/printpdf)
|
- PDF generation with [printpdf](https://github.com/fschutt/printpdf)
|
||||||
- Image processing with [image-rs](https://github.com/image-rs/image)
|
- Image processing with [image-rs](https://github.com/image-rs/image)
|
||||||
|
|||||||
@@ -0,0 +1,553 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>DOCX MCP Server - Web Interface</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background: #1a73e8;
|
||||||
|
color: white;
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header p {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel h2 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: #1a73e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-card {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-card:hover {
|
||||||
|
border-color: #1a73e8;
|
||||||
|
box-shadow: 0 2px 8px rgba(26, 115, 232, 0.2);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-card h3 {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #1a73e8;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-card p {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input,
|
||||||
|
.form-group textarea,
|
||||||
|
.form-group select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group textarea {
|
||||||
|
min-height: 200px;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: #1a73e8;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: #1557b0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #f1f1f1;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-panel {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 1rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-panel pre {
|
||||||
|
margin: 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.success {
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.error {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.loading {
|
||||||
|
background: #fff3cd;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-status {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-status.connected {
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-status.disconnected {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<h1>DOCX MCP Server</h1>
|
||||||
|
<p>Word Document Processing Interface</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="panel">
|
||||||
|
<h2>Templates</h2>
|
||||||
|
<div id="templatesPanel">
|
||||||
|
<p>Loading templates...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<h2>Available Tools</h2>
|
||||||
|
<div class="tool-grid" id="toolGrid">
|
||||||
|
<div style="text-align: center; padding: 2rem;">
|
||||||
|
<p>Loading tools...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel" id="toolFormPanel" style="display: none;">
|
||||||
|
<h2 id="toolName">Tool Name</h2>
|
||||||
|
<p id="toolDescription" style="margin-bottom: 1rem; color: #666;"></p>
|
||||||
|
|
||||||
|
<div id="toolForm">
|
||||||
|
<!-- Form fields will be generated here -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 1rem;">
|
||||||
|
<button class="btn btn-primary" onclick="executeTool()">Execute</button>
|
||||||
|
<button class="btn btn-secondary" onclick="resetForm()">Reset</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel" id="responsePanel" style="display: none;">
|
||||||
|
<h2>Response</h2>
|
||||||
|
<div id="responseStatus"></div>
|
||||||
|
<div class="response-panel">
|
||||||
|
<pre id="responseContent"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="connection-status" id="connectionStatus">
|
||||||
|
Connecting...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let currentTool = null;
|
||||||
|
let tools = [];
|
||||||
|
let ws = null;
|
||||||
|
|
||||||
|
// Initialize on page load
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
loadTools();
|
||||||
|
loadTemplates();
|
||||||
|
connectWebSocket();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load available templates
|
||||||
|
async function loadTemplates() {
|
||||||
|
const container = document.getElementById('templatesPanel');
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/call', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ name: 'list_templates', arguments: {} })
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
if (!data.success || !data.content || !data.content.templates || data.content.templates.length === 0) {
|
||||||
|
container.innerHTML = '<p>No templates available.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const list = document.createElement('div');
|
||||||
|
list.style.display = 'flex';
|
||||||
|
list.style.flexWrap = 'wrap';
|
||||||
|
list.style.gap = '0.5rem';
|
||||||
|
data.content.templates.forEach(t => {
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.className = 'btn btn-secondary';
|
||||||
|
btn.textContent = t;
|
||||||
|
btn.onclick = () => openTemplate(t);
|
||||||
|
list.appendChild(btn);
|
||||||
|
});
|
||||||
|
container.appendChild(list);
|
||||||
|
} catch (err) {
|
||||||
|
container.innerHTML = '<p>Failed to load templates.</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open a template via the server
|
||||||
|
async function openTemplate(name) {
|
||||||
|
const responsePanel = document.getElementById('responsePanel');
|
||||||
|
const status = document.getElementById('responseStatus');
|
||||||
|
const content = document.getElementById('responseContent');
|
||||||
|
responsePanel.style.display = 'block';
|
||||||
|
status.innerHTML = '<span class="status loading">Opening template...</span>';
|
||||||
|
content.textContent = '';
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/call', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ name: 'open_template', arguments: { name } })
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
status.innerHTML = '<span class="status success">Template opened</span>';
|
||||||
|
content.textContent = JSON.stringify(data.content, null, 2);
|
||||||
|
} else {
|
||||||
|
status.innerHTML = '<span class="status error">Error</span>';
|
||||||
|
content.textContent = data.error || JSON.stringify(data, null, 2);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
status.innerHTML = '<span class="status error">Error</span>';
|
||||||
|
content.textContent = err.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load available tools
|
||||||
|
async function loadTools() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/tools');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
tools = data.tools;
|
||||||
|
renderToolGrid();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load tools:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render tool cards
|
||||||
|
function renderToolGrid() {
|
||||||
|
const grid = document.getElementById('toolGrid');
|
||||||
|
grid.innerHTML = '';
|
||||||
|
|
||||||
|
tools.forEach(tool => {
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'tool-card';
|
||||||
|
card.onclick = () => selectTool(tool);
|
||||||
|
|
||||||
|
card.innerHTML = `
|
||||||
|
<h3>${tool.name}</h3>
|
||||||
|
<p>${tool.description || 'No description available'}</p>
|
||||||
|
`;
|
||||||
|
|
||||||
|
grid.appendChild(card);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select a tool to use
|
||||||
|
function selectTool(tool) {
|
||||||
|
currentTool = tool;
|
||||||
|
|
||||||
|
document.getElementById('toolName').textContent = tool.name;
|
||||||
|
document.getElementById('toolDescription').textContent = tool.description;
|
||||||
|
|
||||||
|
// Generate form based on input schema
|
||||||
|
generateForm(tool.input_schema);
|
||||||
|
|
||||||
|
document.getElementById('toolFormPanel').style.display = 'block';
|
||||||
|
document.getElementById('responsePanel').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate form fields from schema
|
||||||
|
function generateForm(schema) {
|
||||||
|
const form = document.getElementById('toolForm');
|
||||||
|
form.innerHTML = '';
|
||||||
|
|
||||||
|
if (!schema.properties) return;
|
||||||
|
|
||||||
|
Object.entries(schema.properties).forEach(([name, prop]) => {
|
||||||
|
const group = document.createElement('div');
|
||||||
|
group.className = 'form-group';
|
||||||
|
|
||||||
|
const label = document.createElement('label');
|
||||||
|
label.textContent = `${name}${schema.required && schema.required.includes(name) ? ' *' : ''}`;
|
||||||
|
|
||||||
|
let input;
|
||||||
|
switch (prop.type) {
|
||||||
|
case 'string':
|
||||||
|
if (prop.enum) {
|
||||||
|
input = document.createElement('select');
|
||||||
|
input.id = `field_${name}`;
|
||||||
|
input.innerHTML = '<option value="">Select...</option>';
|
||||||
|
prop.enum.forEach(option => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = option;
|
||||||
|
opt.textContent = option;
|
||||||
|
input.appendChild(opt);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
input = document.createElement('textarea');
|
||||||
|
input.id = `field_${name}`;
|
||||||
|
input.placeholder = prop.description || `Enter ${name}`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'boolean':
|
||||||
|
input = document.createElement('input');
|
||||||
|
input.type = 'checkbox';
|
||||||
|
input.id = `field_${name}`;
|
||||||
|
break;
|
||||||
|
case 'number':
|
||||||
|
case 'integer':
|
||||||
|
input = document.createElement('input');
|
||||||
|
input.type = 'number';
|
||||||
|
input.id = `field_${name}`;
|
||||||
|
input.placeholder = prop.description || `Enter ${name}`;
|
||||||
|
break;
|
||||||
|
case 'array':
|
||||||
|
case 'object':
|
||||||
|
input = document.createElement('textarea');
|
||||||
|
input.id = `field_${name}`;
|
||||||
|
input.placeholder = prop.description || `Enter JSON for ${name}`;
|
||||||
|
input.style.fontFamily = 'monospace';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
input = document.createElement('input');
|
||||||
|
input.id = `field_${name}`;
|
||||||
|
input.placeholder = prop.description || `Enter ${name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
group.appendChild(label);
|
||||||
|
group.appendChild(input);
|
||||||
|
form.appendChild(group);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute tool call
|
||||||
|
async function executeTool() {
|
||||||
|
if (!currentTool) return;
|
||||||
|
|
||||||
|
const status = document.getElementById('responseStatus');
|
||||||
|
const content = document.getElementById('responseContent');
|
||||||
|
|
||||||
|
status.innerHTML = '<span class="status loading">Executing...</span>';
|
||||||
|
content.textContent = '';
|
||||||
|
document.getElementById('responsePanel').style.display = 'block';
|
||||||
|
|
||||||
|
// Collect form data
|
||||||
|
const arguments = {};
|
||||||
|
const schema = currentTool.input_schema;
|
||||||
|
|
||||||
|
if (schema.properties) {
|
||||||
|
Object.entries(schema.properties).forEach(([name, prop]) => {
|
||||||
|
const field = document.getElementById(`field_${name}`);
|
||||||
|
if (field) {
|
||||||
|
let value;
|
||||||
|
switch (prop.type) {
|
||||||
|
case 'boolean':
|
||||||
|
value = field.checked;
|
||||||
|
break;
|
||||||
|
case 'number':
|
||||||
|
case 'integer':
|
||||||
|
value = parseInt(field.value) || field.value;
|
||||||
|
break;
|
||||||
|
case 'array':
|
||||||
|
case 'object':
|
||||||
|
try {
|
||||||
|
value = JSON.parse(field.value);
|
||||||
|
} catch {
|
||||||
|
value = field.value;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
value = field.value;
|
||||||
|
}
|
||||||
|
if (value || value === false) {
|
||||||
|
arguments[name] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/call', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: currentTool.name,
|
||||||
|
arguments: arguments
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
status.innerHTML = '<span class="status success">Success</span>';
|
||||||
|
content.textContent = JSON.stringify(data.content, null, 2);
|
||||||
|
} else {
|
||||||
|
status.innerHTML = '<span class="status error">Error</span>';
|
||||||
|
content.textContent = data.error || 'Unknown error occurred';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
status.innerHTML = '<span class="status error">Error</span>';
|
||||||
|
content.textContent = error.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
function resetForm() {
|
||||||
|
const fields = document.querySelectorAll('#toolForm input, #toolForm textarea, #toolForm select');
|
||||||
|
fields.forEach(field => {
|
||||||
|
if (field.type === 'checkbox') {
|
||||||
|
field.checked = false;
|
||||||
|
} else {
|
||||||
|
field.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebSocket connection for real-time updates
|
||||||
|
function connectWebSocket() {
|
||||||
|
const status = document.getElementById('connectionStatus');
|
||||||
|
|
||||||
|
try {
|
||||||
|
ws = new WebSocket(`ws://${window.location.host}/ws`);
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
status.textContent = 'Connected';
|
||||||
|
status.className = 'connection-status connected';
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
status.textContent = 'Disconnected';
|
||||||
|
status.className = 'connection-status disconnected';
|
||||||
|
setTimeout(connectWebSocket, 5000);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = (error) => {
|
||||||
|
console.error('WebSocket error:', error);
|
||||||
|
status.textContent = 'Connection Error';
|
||||||
|
status.className = 'connection-status disconnected';
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to connect WebSocket:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
# Production deployment for docx-mcp server
|
||||||
|
# This creates a sandboxed environment with optional WebSocket bridge for remote access
|
||||||
|
|
||||||
|
services:
|
||||||
|
# WebSocket bridge for remote access (optional)
|
||||||
|
# Comment out this service if using local stdio transport only
|
||||||
|
websockify:
|
||||||
|
image: websockify/websockify
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
depends_on:
|
||||||
|
- docx-mcp
|
||||||
|
command: ["--web", "/dev/null", "8080", "docx-mcp:8080"]
|
||||||
|
networks:
|
||||||
|
- docx-network
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "nc", "-z", "localhost", "8080"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
# Main docx-mcp server
|
||||||
|
docx-mcp:
|
||||||
|
image: docx-mcp:sandboxed
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.sandboxed
|
||||||
|
read_only: true
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
|
cap_add:
|
||||||
|
- NET_BIND_SERVICE # Only if exposing port for bridge
|
||||||
|
tmpfs:
|
||||||
|
- /tmp/docx-mcp:rw,noexec,nosuid,size=100m
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 512M
|
||||||
|
cpus: '1.0'
|
||||||
|
replicas: 1
|
||||||
|
environment:
|
||||||
|
- RUST_LOG=info
|
||||||
|
- DOCX_MCP_SANDBOX=true
|
||||||
|
- DOCX_MCP_NO_EXTERNAL_TOOLS=true
|
||||||
|
- DOCX_MCP_NO_NETWORK=true
|
||||||
|
- DOCX_MCP_MAX_SIZE=52428800
|
||||||
|
- DOCX_MCP_MAX_DOCS=20
|
||||||
|
networks:
|
||||||
|
- docx-network
|
||||||
|
ports:
|
||||||
|
- "8080:8080" # Only needed for WebSocket bridge
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "/usr/local/bin/docx-mcp", "--version"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
networks:
|
||||||
|
docx-network:
|
||||||
|
driver: bridge
|
||||||
+224
-1
@@ -20,6 +20,7 @@ pub struct DocxToolsProvider {
|
|||||||
advanced: Arc<AdvancedDocxHandler>,
|
advanced: Arc<AdvancedDocxHandler>,
|
||||||
security: Arc<SecurityMiddleware>,
|
security: Arc<SecurityMiddleware>,
|
||||||
security_config: SecurityConfig,
|
security_config: SecurityConfig,
|
||||||
|
templates_dir: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DocxToolsProvider {
|
impl DocxToolsProvider {
|
||||||
@@ -28,6 +29,10 @@ impl DocxToolsProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_with_security(security_config: SecurityConfig) -> Self {
|
pub fn new_with_security(security_config: SecurityConfig) -> Self {
|
||||||
|
Self::new_with_security_and_templates(security_config, PathBuf::from("/templates"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_with_security_and_templates(security_config: SecurityConfig, templates_dir: PathBuf) -> Self {
|
||||||
Self {
|
Self {
|
||||||
handler: Arc::new(RwLock::new(DocxHandler::new().expect("Failed to create DocxHandler"))),
|
handler: Arc::new(RwLock::new(DocxHandler::new().expect("Failed to create DocxHandler"))),
|
||||||
converter: Arc::new(DocumentConverter::new()),
|
converter: Arc::new(DocumentConverter::new()),
|
||||||
@@ -35,6 +40,7 @@ impl DocxToolsProvider {
|
|||||||
advanced: Arc::new(AdvancedDocxHandler::new()),
|
advanced: Arc::new(AdvancedDocxHandler::new()),
|
||||||
security: Arc::new(SecurityMiddleware::new(security_config.clone())),
|
security: Arc::new(SecurityMiddleware::new(security_config.clone())),
|
||||||
security_config,
|
security_config,
|
||||||
|
templates_dir,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,6 +58,7 @@ impl DocxToolsProvider {
|
|||||||
advanced: Arc::new(AdvancedDocxHandler::new()),
|
advanced: Arc::new(AdvancedDocxHandler::new()),
|
||||||
security: Arc::new(SecurityMiddleware::new(security_config.clone())),
|
security: Arc::new(SecurityMiddleware::new(security_config.clone())),
|
||||||
security_config,
|
security_config,
|
||||||
|
templates_dir: PathBuf::from("/templates"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -956,6 +963,56 @@ impl DocxToolsProvider {
|
|||||||
}),
|
}),
|
||||||
annotations: None,
|
annotations: None,
|
||||||
},
|
},
|
||||||
|
Tool {
|
||||||
|
name: "list_templates".to_string(),
|
||||||
|
description: Some("List available document templates from the templates directory".to_string()),
|
||||||
|
input_schema: json!({
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
"required": []
|
||||||
|
}),
|
||||||
|
annotations: None,
|
||||||
|
},
|
||||||
|
Tool {
|
||||||
|
name: "open_template".to_string(),
|
||||||
|
description: Some("Open a template document by name from the templates directory".to_string()),
|
||||||
|
input_schema: json!({
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Template file name (e.g., 'nda_template.docx')"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["name"]
|
||||||
|
}),
|
||||||
|
annotations: None,
|
||||||
|
},
|
||||||
|
Tool {
|
||||||
|
name: "generate_from_template".to_string(),
|
||||||
|
description: Some(
|
||||||
|
"Generate a new document from a template by filling placeholders like {{FIELD_NAME}} with provided values".to_string()
|
||||||
|
),
|
||||||
|
input_schema: json!({
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"template_name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Template file name (e.g., 'nda_template.docx')"
|
||||||
|
},
|
||||||
|
"output_path": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Output DOCX path (e.g., '/out/nda_filled.docx')"
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Key-value pairs; keys are placeholder names without braces. Example: {\"CLIENT_NAME\": \"Acme Corp\"}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["template_name", "output_path", "fields"]
|
||||||
|
}),
|
||||||
|
annotations: None,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Filter tools based on security configuration
|
// Filter tools based on security configuration
|
||||||
@@ -1715,7 +1772,173 @@ impl DocxToolsProvider {
|
|||||||
Err(e) => ToolOutcome::Error { code: ErrorCode::InternalError, error: e.to_string(), hint: None },
|
Err(e) => ToolOutcome::Error { code: ErrorCode::InternalError, error: e.to_string(), hint: None },
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"list_templates" => {
|
||||||
|
let mut templates = Vec::new();
|
||||||
|
if self.templates_dir.exists() {
|
||||||
|
if let Ok(entries) = std::fs::read_dir(&self.templates_dir) {
|
||||||
|
for entry in entries.filter_map(|e| e.ok()) {
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_file() {
|
||||||
|
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
|
||||||
|
if ext.eq_ignore_ascii_case("docx") {
|
||||||
|
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
|
||||||
|
templates.push(name.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templates.sort();
|
||||||
|
ToolOutcome::Metadata {
|
||||||
|
metadata: serde_json::json!({ "templates": templates }),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"open_template" => {
|
||||||
|
let name = arguments["name"].as_str().unwrap_or("");
|
||||||
|
if name.is_empty() {
|
||||||
|
ToolOutcome::Error {
|
||||||
|
code: ErrorCode::ValidationError,
|
||||||
|
error: "Template name is required".to_string(),
|
||||||
|
hint: Some("Provide 'name' with a .docx filename from list_templates".to_string()),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let path = self.templates_dir.join(name);
|
||||||
|
if !path.exists() || !path.is_file() {
|
||||||
|
ToolOutcome::Error {
|
||||||
|
code: ErrorCode::ValidationError,
|
||||||
|
error: format!("Template not found: {}", name),
|
||||||
|
hint: Some("Check list_templates for available names".to_string()),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let mut handler = self.handler.write().unwrap();
|
||||||
|
match handler.open_document(&path) {
|
||||||
|
Ok(doc_id) => ToolOutcome::Created {
|
||||||
|
document_id: doc_id,
|
||||||
|
message: Some(format!("Opened template '{}' as document", name)),
|
||||||
|
},
|
||||||
|
Err(e) => ToolOutcome::Error {
|
||||||
|
code: ErrorCode::InternalError,
|
||||||
|
error: e.to_string(),
|
||||||
|
hint: None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"generate_from_template" => {
|
||||||
|
let template_name = arguments["template_name"].as_str().unwrap_or("");
|
||||||
|
let output_path = arguments["output_path"].as_str().unwrap_or("");
|
||||||
|
let fields = arguments.get("fields").and_then(|v| v.as_object()).cloned().unwrap_or_default();
|
||||||
|
|
||||||
|
if template_name.is_empty() {
|
||||||
|
ToolOutcome::Error {
|
||||||
|
code: ErrorCode::ValidationError,
|
||||||
|
error: "template_name is required".to_string(),
|
||||||
|
hint: Some("Provide the template file name from list_templates".to_string()),
|
||||||
|
}
|
||||||
|
} else if output_path.is_empty() {
|
||||||
|
ToolOutcome::Error {
|
||||||
|
code: ErrorCode::ValidationError,
|
||||||
|
error: "output_path is required".to_string(),
|
||||||
|
hint: Some("Provide an absolute path where the generated DOCX will be saved".to_string()),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let template_path = self.templates_dir.join(template_name);
|
||||||
|
if !template_path.exists() || !template_path.is_file() {
|
||||||
|
ToolOutcome::Error {
|
||||||
|
code: ErrorCode::ValidationError,
|
||||||
|
error: format!("Template not found: {}", template_name),
|
||||||
|
hint: Some("Check list_templates for available names".to_string()),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Open template
|
||||||
|
let mut handler = self.handler.write().unwrap();
|
||||||
|
let doc_id = match handler.open_document(&template_path) {
|
||||||
|
Ok(id) => id,
|
||||||
|
Err(e) => {
|
||||||
|
drop(handler);
|
||||||
|
return ToolOutcome::Error {
|
||||||
|
code: ErrorCode::InternalError,
|
||||||
|
error: e.to_string(),
|
||||||
|
hint: None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply field replacements
|
||||||
|
let mut replace_count = 0usize;
|
||||||
|
for (key, value) in &fields {
|
||||||
|
let placeholder = format!("{{{{{}}}}}", key);
|
||||||
|
let val_str = match value {
|
||||||
|
Value::String(s) => s.clone(),
|
||||||
|
_ => value.to_string(),
|
||||||
|
};
|
||||||
|
if let Ok(count) = handler.find_and_replace_advanced(
|
||||||
|
&doc_id,
|
||||||
|
&placeholder,
|
||||||
|
&val_str,
|
||||||
|
false, // case_sensitive: false for placeholders
|
||||||
|
true, // whole_word: true (treat placeholder as whole token)
|
||||||
|
false, // use_regex: false
|
||||||
|
) {
|
||||||
|
replace_count += count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save generated document
|
||||||
|
let out_path = PathBuf::from(output_path);
|
||||||
|
let result = if out_path.parent().is_some() {
|
||||||
|
if let Err(e) = std::fs::create_dir_all(out_path.parent().unwrap()) {
|
||||||
|
drop(handler);
|
||||||
|
ToolOutcome::Error {
|
||||||
|
code: ErrorCode::InternalError,
|
||||||
|
error: format!("Failed to create output directory: {}", e),
|
||||||
|
hint: None,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match handler.save_document(&doc_id, &out_path) {
|
||||||
|
Ok(()) => ToolOutcome::Ok {
|
||||||
|
message: Some(format!(
|
||||||
|
"Generated document from template '{}' with {} replacements at {}",
|
||||||
|
template_name, replace_count, output_path
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
Err(e) => ToolOutcome::Error {
|
||||||
|
code: ErrorCode::InternalError,
|
||||||
|
error: e.to_string(),
|
||||||
|
hint: None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match handler.save_document(&doc_id, &out_path) {
|
||||||
|
Ok(()) => ToolOutcome::Ok {
|
||||||
|
message: Some(format!(
|
||||||
|
"Generated document from template '{}' with {} replacements at {}",
|
||||||
|
template_name, replace_count, output_path
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
Err(e) => ToolOutcome::Error {
|
||||||
|
code: ErrorCode::InternalError,
|
||||||
|
error: e.to_string(),
|
||||||
|
hint: None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Optionally close template document
|
||||||
|
let _ = handler.close_document(&doc_id);
|
||||||
|
drop(handler);
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
_ => {
|
_ => {
|
||||||
ToolOutcome::Error { code: ErrorCode::UnknownTool, error: format!("Unknown or unsupported tool: {}", name), hint: None }
|
ToolOutcome::Error { code: ErrorCode::UnknownTool, error: format!("Unknown or unsupported tool: {}", name), hint: None }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,202 @@
|
|||||||
|
use axum::{
|
||||||
|
extract::{
|
||||||
|
ws::{Message, WebSocket},
|
||||||
|
State, WebSocketUpgrade,
|
||||||
|
},
|
||||||
|
response::{Html, Response},
|
||||||
|
routing::{get, post},
|
||||||
|
Router,
|
||||||
|
Json,
|
||||||
|
};
|
||||||
|
use futures::{SinkExt, StreamExt};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{
|
||||||
|
net::SocketAddr,
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
use tower_http::cors::{Any, CorsLayer};
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
use crate::docx_tools::DocxToolsProvider;
|
||||||
|
|
||||||
|
/// Application state shared across HTTP handlers
|
||||||
|
pub struct AppState {
|
||||||
|
pub provider: DocxToolsProvider,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request to call a tool
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct ToolCallRequest {
|
||||||
|
pub name: String,
|
||||||
|
pub arguments: serde_json::Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Response from a tool call
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct ToolCallResponse {
|
||||||
|
pub success: bool,
|
||||||
|
pub content: serde_json::Value,
|
||||||
|
pub error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Response with list of tools
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct ListToolsResponse {
|
||||||
|
pub success: bool,
|
||||||
|
pub tools: Vec<serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start the HTTP server
|
||||||
|
pub async fn start_http_server(addr: &str, provider: DocxToolsProvider) -> anyhow::Result<()> {
|
||||||
|
let state = Arc::new(AppState { provider });
|
||||||
|
|
||||||
|
let app = Router::new()
|
||||||
|
.state(state.clone())
|
||||||
|
// Serve HTML interface
|
||||||
|
.route("/", get(index_handler))
|
||||||
|
.route("/api/tools", get(list_tools_handler))
|
||||||
|
.route("/api/call", post(call_tool_handler))
|
||||||
|
.route("/ws", get(ws_handler))
|
||||||
|
// CORS policy - allow all origins on LAN
|
||||||
|
.layer(CorsLayer::new().allow_origin(Any()).allow_methods(tower_http::cors::Method::any()));
|
||||||
|
|
||||||
|
let addr = SocketAddr::from_str(addr).unwrap_or_else(|_| {
|
||||||
|
info!("Invalid address format, using default 0.0.0.0:3000");
|
||||||
|
"0.0.0.0:3000".parse().unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
|
info!("Starting HTTP server on {}", addr);
|
||||||
|
|
||||||
|
let listener = tokio::net::TcpListener::bind(addr).await?;
|
||||||
|
axum::serve(listener, app).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serve the HTML interface
|
||||||
|
async fn index_handler() -> Html<String> {
|
||||||
|
Html(include_str!("../assets/html_interface.html").to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List available tools
|
||||||
|
async fn list_tools_handler(State(state): State<Arc<AppState>>) -> Json<ListToolsResponse> {
|
||||||
|
let tools = state.provider.list_tools().await;
|
||||||
|
|
||||||
|
let tool_list: Vec<serde_json::Value> = tools.iter().map(|t| {
|
||||||
|
serde_json::json!({
|
||||||
|
"name": t.name,
|
||||||
|
"description": t.description,
|
||||||
|
"input_schema": t.input_schema
|
||||||
|
})
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
Json(ListToolsResponse {
|
||||||
|
success: true,
|
||||||
|
tools: tool_list,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Call a tool via HTTP POST
|
||||||
|
async fn call_tool_handler(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
Json(request): Json<ToolCallRequest>,
|
||||||
|
) -> Json<ToolCallResponse> {
|
||||||
|
let response = state.provider.call_tool(&request.name, request.arguments).await;
|
||||||
|
|
||||||
|
// Convert response to JSON
|
||||||
|
let content = if let Some(content) = response.content.first() {
|
||||||
|
match content {
|
||||||
|
mcp_core::types::ToolResponseContent::Text(text) => {
|
||||||
|
serde_json::from_str(&text.text).unwrap_or_else(|_| {
|
||||||
|
serde_json::json!({"text": text.text.clone()})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
mcp_core::types::ToolResponseContent::Image(image) => {
|
||||||
|
serde_json::json!({
|
||||||
|
"data": image.data,
|
||||||
|
"mimeType": image.mime_type
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
serde_json::json!({})
|
||||||
|
};
|
||||||
|
|
||||||
|
Json(ToolCallResponse {
|
||||||
|
success: response.is_error.unwrap_or(false) == false,
|
||||||
|
content,
|
||||||
|
error: response.is_error.unwrap_or(false).then(|| "Tool call failed".to_string()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// WebSocket handler for real-time communication
|
||||||
|
async fn ws_handler(
|
||||||
|
ws: WebSocketUpgrade,
|
||||||
|
State(state): State<Arc<AppState>>
|
||||||
|
) -> Result<Response, axum::http::StatusCode> {
|
||||||
|
ws.on_upgrade(move |socket| async move {
|
||||||
|
let provider = state.provider.clone();
|
||||||
|
let mut ws = socket;
|
||||||
|
|
||||||
|
// Handle WebSocket messages
|
||||||
|
while let Some(msg) = ws.recv().await {
|
||||||
|
let msg = match msg {
|
||||||
|
Ok(msg) => msg,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
let text = match msg {
|
||||||
|
Message::Text(text) => text.to_string(),
|
||||||
|
_ => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse request
|
||||||
|
let request: ToolCallRequest = match serde_json::from_str(&text) {
|
||||||
|
Ok(req) => req,
|
||||||
|
Err(e) => {
|
||||||
|
let error_response = ToolCallResponse {
|
||||||
|
success: false,
|
||||||
|
content: serde_json::json!({}),
|
||||||
|
error: Some(format!("Parse error: {}", e)),
|
||||||
|
};
|
||||||
|
let _ = ws.send(Message::Text(
|
||||||
|
serde_json::to_string(&error_response).unwrap_or("{}".to_string())
|
||||||
|
)).await;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Call tool
|
||||||
|
let response = provider.call_tool(&request.name, request.arguments).await;
|
||||||
|
|
||||||
|
// Convert response to JSON
|
||||||
|
let content = if let Some(content) = response.content.first() {
|
||||||
|
match content {
|
||||||
|
mcp_core::types::ToolResponseContent::Text(text) => {
|
||||||
|
serde_json::from_str(&text.text).unwrap_or_else(|_| {
|
||||||
|
serde_json::json!({"text": text.text.clone()})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
mcp_core::types::ToolResponseContent::Image(image) => {
|
||||||
|
serde_json::json!({
|
||||||
|
"data": image.data,
|
||||||
|
"mimeType": image.mime_type
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
serde_json::json!({})
|
||||||
|
};
|
||||||
|
|
||||||
|
let ws_response = ToolCallResponse {
|
||||||
|
success: response.is_error.unwrap_or(false) == false,
|
||||||
|
content,
|
||||||
|
error: response.is_error.unwrap_or(false).then(|| "Tool call failed".to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = ws.send(Message::Text(
|
||||||
|
serde_json::to_string(&ws_response).unwrap_or("{}".to_string())
|
||||||
|
)).await;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
+37
-8
@@ -15,6 +15,8 @@ mod converter;
|
|||||||
mod pure_converter;
|
mod pure_converter;
|
||||||
#[cfg(all(feature = "runtime-server", feature = "advanced-docx"))]
|
#[cfg(all(feature = "runtime-server", feature = "advanced-docx"))]
|
||||||
mod advanced_docx;
|
mod advanced_docx;
|
||||||
|
#[cfg(feature = "http-server")]
|
||||||
|
mod http_server;
|
||||||
mod security;
|
mod security;
|
||||||
|
|
||||||
#[cfg(feature = "embedded-fonts")]
|
#[cfg(feature = "embedded-fonts")]
|
||||||
@@ -53,6 +55,39 @@ async fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if HTTP mode is enabled before consuming args
|
||||||
|
let http_mode = args.http_mode;
|
||||||
|
let http_address = args.http_address.clone();
|
||||||
|
let templates_dir = args.templates_dir.clone();
|
||||||
|
|
||||||
|
// Create the tools provider
|
||||||
|
let security_config = security::SecurityConfig::from_args(args);
|
||||||
|
info!("Starting DOCX MCP Server - Security: {}", security_config.get_summary());
|
||||||
|
info!("Templates directory: {}", templates_dir);
|
||||||
|
|
||||||
|
let provider = DocxToolsProvider::new_with_security_and_templates(
|
||||||
|
security_config,
|
||||||
|
std::path::PathBuf::from(&templates_dir),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if HTTP mode is enabled
|
||||||
|
if http_mode {
|
||||||
|
#[cfg(feature = "http-server")]
|
||||||
|
{
|
||||||
|
let addr = http_address.unwrap_or_else(|| "0.0.0.0:3000".to_string());
|
||||||
|
info!("Starting in HTTP mode on {}", addr);
|
||||||
|
return http_server::start_http_server(&addr, provider).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "http-server"))]
|
||||||
|
{
|
||||||
|
eprintln!("HTTP mode requires the 'http-server' feature to be enabled during build.");
|
||||||
|
eprintln!("Rebuild with: cargo build --release --features http-server");
|
||||||
|
return Err(anyhow::anyhow!("HTTP mode not available"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: stdio mode
|
||||||
#[cfg(feature = "runtime-server")]
|
#[cfg(feature = "runtime-server")]
|
||||||
{
|
{
|
||||||
use mcp_server::{Router, Server};
|
use mcp_server::{Router, Server};
|
||||||
@@ -67,9 +102,6 @@ async fn main() -> Result<()> {
|
|||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use tokio::io::{stdin, stdout};
|
use tokio::io::{stdin, stdout};
|
||||||
|
|
||||||
let security_config = security::SecurityConfig::from_args(args);
|
|
||||||
info!("Starting DOCX MCP Server - Security: {}", security_config.get_summary());
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct DocxRouter(docx_tools::DocxToolsProvider);
|
struct DocxRouter(docx_tools::DocxToolsProvider);
|
||||||
|
|
||||||
@@ -80,7 +112,6 @@ async fn main() -> Result<()> {
|
|||||||
CapabilitiesBuilder::new().with_tools(true).build()
|
CapabilitiesBuilder::new().with_tools(true).build()
|
||||||
}
|
}
|
||||||
fn list_tools(&self) -> Vec<SpecTool> {
|
fn list_tools(&self) -> Vec<SpecTool> {
|
||||||
// DocxToolsProvider::list_tools is async; block briefly with tokio runtime handle
|
|
||||||
let rt = tokio::runtime::Handle::current();
|
let rt = tokio::runtime::Handle::current();
|
||||||
let tools = rt.block_on(self.0.list_tools());
|
let tools = rt.block_on(self.0.list_tools());
|
||||||
tools.into_iter().map(|t| SpecTool{ name: t.name, description: t.description.unwrap_or_default(), input_schema: t.input_schema }).collect()
|
tools.into_iter().map(|t| SpecTool{ name: t.name, description: t.description.unwrap_or_default(), input_schema: t.input_schema }).collect()
|
||||||
@@ -90,7 +121,6 @@ async fn main() -> Result<()> {
|
|||||||
let name = tool_name.to_string();
|
let name = tool_name.to_string();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let resp = provider.call_tool(&name, arguments).await;
|
let resp = provider.call_tool(&name, arguments).await;
|
||||||
// Convert our CallToolResponse (text JSON) to Content::text
|
|
||||||
let text = match resp.content.get(0) {
|
let text = match resp.content.get(0) {
|
||||||
Some(mcp_core::types::ToolResponseContent::Text(t)) => t.text.clone(),
|
Some(mcp_core::types::ToolResponseContent::Text(t)) => t.text.clone(),
|
||||||
_ => serde_json::to_string(&resp).unwrap_or_else(|_| "{}".to_string()),
|
_ => serde_json::to_string(&resp).unwrap_or_else(|_| "{}".to_string()),
|
||||||
@@ -108,7 +138,7 @@ async fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let router = DocxRouter(DocxToolsProvider::new_with_security(security_config));
|
let router = DocxRouter(provider);
|
||||||
let service = RouterService(router);
|
let service = RouterService(router);
|
||||||
let server = Server::new(service);
|
let server = Server::new(service);
|
||||||
let transport = mcp_server::ByteTransport::new(stdin(), stdout());
|
let transport = mcp_server::ByteTransport::new(stdin(), stdout());
|
||||||
@@ -117,9 +147,8 @@ async fn main() -> Result<()> {
|
|||||||
|
|
||||||
#[cfg(not(feature = "runtime-server"))]
|
#[cfg(not(feature = "runtime-server"))]
|
||||||
{
|
{
|
||||||
// No runtime server compiled in; if no subcommand was used, exit with guidance
|
|
||||||
eprintln!("Runtime server disabled. Rebuild with --features runtime-server to run the MCP server.");
|
eprintln!("Runtime server disabled. Rebuild with --features runtime-server to run the MCP server.");
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,18 @@ pub struct Args {
|
|||||||
#[arg(long, env = "DOCX_MCP_MAX_DOCS")]
|
#[arg(long, env = "DOCX_MCP_MAX_DOCS")]
|
||||||
pub max_docs: Option<usize>,
|
pub max_docs: Option<usize>,
|
||||||
|
|
||||||
|
/// Enable HTTP server mode for HTML interface
|
||||||
|
#[arg(long, env = "DOCX_MCP_HTTP")]
|
||||||
|
pub http_mode: bool,
|
||||||
|
|
||||||
|
/// HTTP server address and port (default: 0.0.0.0:3000)
|
||||||
|
#[arg(long, env = "DOCX_MCP_HTTP_ADDRESS")]
|
||||||
|
pub http_address: Option<String>,
|
||||||
|
|
||||||
|
/// Path to directory containing template .docx files
|
||||||
|
#[arg(long, env = "DOCX_MCP_TEMPLATES_DIR", default_value = "/templates")]
|
||||||
|
pub templates_dir: String,
|
||||||
|
|
||||||
/// Optional top-level subcommand (e.g., fonts download)
|
/// Optional top-level subcommand (e.g., fonts download)
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
pub command: Option<CliCommand>,
|
pub command: Option<CliCommand>,
|
||||||
|
|||||||
Reference in New Issue
Block a user