docforge-mcp 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- config.py +389 -0
- docforge_mcp-0.1.0.dist-info/METADATA +134 -0
- docforge_mcp-0.1.0.dist-info/RECORD +59 -0
- docforge_mcp-0.1.0.dist-info/WHEEL +4 -0
- docforge_mcp-0.1.0.dist-info/entry_points.txt +3 -0
- docforge_mcp-0.1.0.dist-info/licenses/LICENSE +22 -0
- docx_tools/__init__.py +5 -0
- docx_tools/advanced/__init__.py +19 -0
- docx_tools/advanced/features.py +101 -0
- docx_tools/base_docx_tool.py +226 -0
- docx_tools/conditional_templates.py +112 -0
- docx_tools/dynamic_docx_tools.py +539 -0
- docx_tools/helpers.py +703 -0
- edit_tools/__init__.py +16 -0
- edit_tools/docx_editor.py +94 -0
- edit_tools/pptx_editor.py +60 -0
- edit_tools/xlsx_editor.py +53 -0
- email_tools/__init__.py +10 -0
- email_tools/base_email_tool.py +114 -0
- email_tools/dynamic_email_tools.py +177 -0
- mcp_office_documents/__init__.py +3 -0
- mcp_office_documents/app.py +826 -0
- mcp_office_documents/server.py +40 -0
- merge_tools/__init__.py +5 -0
- merge_tools/merger.py +45 -0
- middleware.py +110 -0
- pdf_tools/__init__.py +5 -0
- pdf_tools/base_pdf_tool.py +131 -0
- pptx_tools/__init__.py +15 -0
- pptx_tools/advanced/__init__.py +10 -0
- pptx_tools/advanced/features.py +97 -0
- pptx_tools/base_pptx_tool.py +44 -0
- pptx_tools/chart_utils.py +177 -0
- pptx_tools/constants.py +52 -0
- pptx_tools/helpers.py +459 -0
- pptx_tools/image_utils.py +227 -0
- pptx_tools/slide_builder.py +398 -0
- read_tools/__init__.py +11 -0
- read_tools/docx_reader.py +63 -0
- read_tools/pptx_reader.py +62 -0
- read_tools/xlsx_reader.py +65 -0
- template_utils.py +133 -0
- upload_tools/__init__.py +4 -0
- upload_tools/backends/__init__.py +13 -0
- upload_tools/backends/azure.py +66 -0
- upload_tools/backends/gcs.py +75 -0
- upload_tools/backends/local.py +28 -0
- upload_tools/backends/minio.py +59 -0
- upload_tools/backends/s3.py +139 -0
- upload_tools/main.py +79 -0
- upload_tools/utils.py +60 -0
- xlsx_tools/__init__.py +3 -0
- xlsx_tools/base_xlsx_tool.py +212 -0
- xlsx_tools/charts/__init__.py +5 -0
- xlsx_tools/charts/chart_builder.py +113 -0
- xlsx_tools/formatting.py +86 -0
- xlsx_tools/helpers.py +383 -0
- xml_tools/__init__.py +4 -0
- xml_tools/base_xml_tool.py +116 -0
config.py
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
"""Centralized configuration and logging setup for the MCP Office Documents server.
|
|
2
|
+
|
|
3
|
+
This module is the single source of truth for reading and validating environment
|
|
4
|
+
variables. No other module should access os.environ directly.
|
|
5
|
+
|
|
6
|
+
Highlights:
|
|
7
|
+
- Reads all env vars and constructs a typed Config instance (Pydantic v2).
|
|
8
|
+
- Validates required settings based on chosen upload strategy (LOCAL/S3/GCS/AZURE).
|
|
9
|
+
- Configures global logging (format and level) exactly once on first access.
|
|
10
|
+
- Exposes get_config() to retrieve a singleton Config across the app.
|
|
11
|
+
|
|
12
|
+
Environment variables (see .env.example for full list):
|
|
13
|
+
- Logging: DEBUG (true/false)
|
|
14
|
+
- Storage generic: UPLOAD_STRATEGY, SIGNED_URL_EXPIRES_IN
|
|
15
|
+
- Strategy specific: AWS_*, GCS_*, AZURE_*
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
import os
|
|
22
|
+
from enum import Enum
|
|
23
|
+
from typing import Optional
|
|
24
|
+
|
|
25
|
+
from dotenv import load_dotenv
|
|
26
|
+
from pydantic import BaseModel, Field, ValidationError, model_validator
|
|
27
|
+
|
|
28
|
+
# Load .env file if present (local development). Existing env vars are NOT
|
|
29
|
+
# overwritten, so in Docker (where env_file sets them) this is a safe no-op.
|
|
30
|
+
load_dotenv()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class LogLevel(str, Enum):
|
|
34
|
+
"""Application log levels (restricted to INFO and DEBUG)."""
|
|
35
|
+
DEBUG = "DEBUG"
|
|
36
|
+
INFO = "INFO"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class LoggingSettings(BaseModel):
|
|
40
|
+
"""Logging configuration simplified to a single DEBUG flag.
|
|
41
|
+
|
|
42
|
+
Behavior:
|
|
43
|
+
- If DEBUG env var is truthy (1/true/on), logging is DEBUG.
|
|
44
|
+
- Otherwise logging is INFO.
|
|
45
|
+
|
|
46
|
+
Exposes convenience properties used across the app:
|
|
47
|
+
- level_no: numeric logging level
|
|
48
|
+
- mcp_level_str: lower-case string for FastMCP's `log_level` argument
|
|
49
|
+
"""
|
|
50
|
+
debug: bool = Field(default=False, description="True to enable DEBUG level, False for INFO")
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def level_no(self) -> int:
|
|
54
|
+
return logging.DEBUG if self.debug else logging.INFO
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def mcp_level_str(self) -> str:
|
|
58
|
+
"""Return lower-case string for FastMCP `log_level` argument."""
|
|
59
|
+
return "debug" if self.debug else "info"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class S3Settings(BaseModel):
|
|
63
|
+
"""Configuration for AWS S3 uploads.
|
|
64
|
+
|
|
65
|
+
Credential resolution follows a layered approach:
|
|
66
|
+
|
|
67
|
+
1. **Explicit credentials** — Set ``AWS_ACCESS_KEY`` and
|
|
68
|
+
``AWS_SECRET_ACCESS_KEY`` environment variables. Can be used for local
|
|
69
|
+
development or non-AWS environments. ``AWS_REGION`` is also required
|
|
70
|
+
when using explicit credentials.
|
|
71
|
+
2. **AWS default credential chain** — When
|
|
72
|
+
the explicit credential env vars are *NOT* set, the boto3 SDK
|
|
73
|
+
automatically discovers credentials from (in order):
|
|
74
|
+
- Environment variables (``AWS_ACCESS_KEY_ID``, ``AWS_SECRET_ACCESS_KEY``,
|
|
75
|
+
``AWS_SESSION_TOKEN``, ``AWS_DEFAULT_REGION``)
|
|
76
|
+
- Shared credential / config files (``~/.aws/credentials``,
|
|
77
|
+
``~/.aws/config``)
|
|
78
|
+
- **AWS SSO / ``aws sso login`` sessions** — for local
|
|
79
|
+
development after running ``aws sso login``
|
|
80
|
+
- ECS container credentials
|
|
81
|
+
- **IRSA (IAM Roles for Service Accounts)** — for pods running on
|
|
82
|
+
**AWS EKS**
|
|
83
|
+
- EC2 instance metadata (IMDSv2)
|
|
84
|
+
|
|
85
|
+
``S3_BUCKET`` is always required regardless of the credential method.
|
|
86
|
+
``AWS_REGION`` is optional when using the default credential chain — boto3
|
|
87
|
+
will resolve the region from the environment, config files, or instance
|
|
88
|
+
metadata.
|
|
89
|
+
"""
|
|
90
|
+
access_key: Optional[str] = None
|
|
91
|
+
secret_key: Optional[str] = None
|
|
92
|
+
region: Optional[str] = None
|
|
93
|
+
bucket: str
|
|
94
|
+
|
|
95
|
+
@model_validator(mode="after")
|
|
96
|
+
def _validate(self) -> "S3Settings":
|
|
97
|
+
"""Validate S3 settings.
|
|
98
|
+
|
|
99
|
+
- ``bucket`` is always required.
|
|
100
|
+
- If explicit credentials are partially provided (only one of
|
|
101
|
+
access_key / secret_key), raise an error — either provide both
|
|
102
|
+
or neither.
|
|
103
|
+
- When explicit credentials are provided, ``region`` is required
|
|
104
|
+
because we construct the endpoint URL from it.
|
|
105
|
+
"""
|
|
106
|
+
# Normalize optional strings by stripping surrounding whitespace and
|
|
107
|
+
# converting empty strings to None.
|
|
108
|
+
if self.access_key is not None:
|
|
109
|
+
self.access_key = self.access_key.strip() or None
|
|
110
|
+
if self.secret_key is not None:
|
|
111
|
+
self.secret_key = self.secret_key.strip() or None
|
|
112
|
+
if self.region is not None:
|
|
113
|
+
self.region = self.region.strip() or None
|
|
114
|
+
|
|
115
|
+
if not self.bucket or not self.bucket.strip():
|
|
116
|
+
raise ValueError("Missing required S3 setting: S3_BUCKET")
|
|
117
|
+
self.bucket = self.bucket.strip()
|
|
118
|
+
|
|
119
|
+
has_key = self.access_key is not None
|
|
120
|
+
has_secret = self.secret_key is not None
|
|
121
|
+
if has_key != has_secret:
|
|
122
|
+
raise ValueError(
|
|
123
|
+
"AWS_ACCESS_KEY and AWS_SECRET_ACCESS_KEY must both be set or both be omitted. "
|
|
124
|
+
"Omit both to use the AWS default credential chain (IRSA, SSO, instance profile, etc.)."
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
if has_key and has_secret and self.region is None:
|
|
128
|
+
raise ValueError(
|
|
129
|
+
"AWS_REGION is required when explicit AWS_ACCESS_KEY / AWS_SECRET_ACCESS_KEY are provided."
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
return self
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def use_explicit_credentials(self) -> bool:
|
|
136
|
+
"""Return True if explicit AWS credentials were provided."""
|
|
137
|
+
return self.access_key is not None and self.secret_key is not None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class GCSSettings(BaseModel):
|
|
141
|
+
"""Required configuration for Google Cloud Storage uploads.
|
|
142
|
+
|
|
143
|
+
When credentials_path is omitted, the client uses Application Default
|
|
144
|
+
Credentials (ADC), which automatically picks up Workload Identity on GKE.
|
|
145
|
+
"""
|
|
146
|
+
bucket: str
|
|
147
|
+
credentials_path: Optional[str] = None
|
|
148
|
+
|
|
149
|
+
@model_validator(mode="after")
|
|
150
|
+
def _non_empty(self) -> "GCSSettings":
|
|
151
|
+
"""Normalize credentials_path and ensure the bucket name is non-empty."""
|
|
152
|
+
# Normalize credentials_path: treat whitespace-only strings as missing.
|
|
153
|
+
if self.credentials_path is not None:
|
|
154
|
+
stripped = str(self.credentials_path).strip()
|
|
155
|
+
self.credentials_path = stripped or None
|
|
156
|
+
|
|
157
|
+
# Validate and normalize bucket.
|
|
158
|
+
bucket_stripped = str(self.bucket).strip()
|
|
159
|
+
if not bucket_stripped:
|
|
160
|
+
raise ValueError("Missing required GCS setting: GCS_BUCKET")
|
|
161
|
+
self.bucket = bucket_stripped
|
|
162
|
+
return self
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class AzureSettings(BaseModel):
|
|
166
|
+
"""Required configuration for Azure Blob Storage uploads.
|
|
167
|
+
|
|
168
|
+
Note: `endpoint` is optional; if empty, defaults to
|
|
169
|
+
https://<account>.blob.core.windows.net
|
|
170
|
+
"""
|
|
171
|
+
account_name: str
|
|
172
|
+
account_key: str
|
|
173
|
+
container: str
|
|
174
|
+
endpoint: Optional[str] = None
|
|
175
|
+
|
|
176
|
+
@model_validator(mode="after")
|
|
177
|
+
def _non_empty(self) -> "AzureSettings":
|
|
178
|
+
"""Ensure all required Azure fields are non-empty."""
|
|
179
|
+
missing = [
|
|
180
|
+
name for name, val in (
|
|
181
|
+
("AZURE_STORAGE_ACCOUNT_NAME", self.account_name),
|
|
182
|
+
("AZURE_STORAGE_ACCOUNT_KEY", self.account_key),
|
|
183
|
+
("AZURE_CONTAINER", self.container),
|
|
184
|
+
) if not str(val).strip()
|
|
185
|
+
]
|
|
186
|
+
if missing:
|
|
187
|
+
raise ValueError(f"Missing required Azure settings: {', '.join(missing)}")
|
|
188
|
+
return self
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class MinioSettings(BaseModel):
|
|
192
|
+
"""Configuration for self-hosted MinIO (S3-compatible) uploads."""
|
|
193
|
+
|
|
194
|
+
endpoint: str = Field(description="Base URL of the MinIO server, e.g., http://minio:9000")
|
|
195
|
+
access_key: str
|
|
196
|
+
secret_key: str
|
|
197
|
+
bucket: str
|
|
198
|
+
region: str = Field(default="us-east-1", description="Region to report to boto3; defaults to us-east-1")
|
|
199
|
+
verify_ssl: bool = Field(default=True, description="Whether to verify SSL certificates when connecting")
|
|
200
|
+
path_style: bool = Field(default=True, description="Use path-style addressing (recommended for MinIO)")
|
|
201
|
+
|
|
202
|
+
@model_validator(mode="after")
|
|
203
|
+
def _non_empty(self) -> "MinioSettings":
|
|
204
|
+
missing = [
|
|
205
|
+
name for name, val in (
|
|
206
|
+
("MINIO_ENDPOINT", self.endpoint),
|
|
207
|
+
("MINIO_ACCESS_KEY", self.access_key),
|
|
208
|
+
("MINIO_SECRET_KEY", self.secret_key),
|
|
209
|
+
("MINIO_BUCKET", self.bucket),
|
|
210
|
+
) if not str(val).strip()
|
|
211
|
+
]
|
|
212
|
+
if missing:
|
|
213
|
+
raise ValueError(f"Missing required MinIO settings: {', '.join(missing)}")
|
|
214
|
+
return self
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class StorageStrategy(str, Enum):
|
|
218
|
+
"""Supported upload backends for produced documents."""
|
|
219
|
+
LOCAL = "LOCAL"
|
|
220
|
+
S3 = "S3"
|
|
221
|
+
GCS = "GCS"
|
|
222
|
+
AZURE = "AZURE"
|
|
223
|
+
MINIO = "MINIO"
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class StorageSettings(BaseModel):
|
|
227
|
+
"""Generic storage configuration plus strategy-specific nested settings.
|
|
228
|
+
|
|
229
|
+
Note: The LOCAL strategy always writes to the working folder ./app/upload;
|
|
230
|
+
there is no configurable output directory for LOCAL.
|
|
231
|
+
"""
|
|
232
|
+
strategy: StorageStrategy = Field(default=StorageStrategy.LOCAL)
|
|
233
|
+
signed_url_expires_in: int = Field(default=3600, gt=0, description="TTL for S3/GCS/Azure download links in seconds")
|
|
234
|
+
|
|
235
|
+
# Optional nested settings depending on strategy
|
|
236
|
+
s3: Optional[S3Settings] = None
|
|
237
|
+
gcs: Optional[GCSSettings] = None
|
|
238
|
+
azure: Optional[AzureSettings] = None
|
|
239
|
+
minio: Optional[MinioSettings] = None
|
|
240
|
+
|
|
241
|
+
@model_validator(mode="after")
|
|
242
|
+
def validate_strategy_requirements(self) -> "StorageSettings":
|
|
243
|
+
"""Ensure required nested settings exist for chosen strategy."""
|
|
244
|
+
if self.strategy == StorageStrategy.S3:
|
|
245
|
+
if not self.s3:
|
|
246
|
+
raise ValueError("S3 settings are required for S3 strategy")
|
|
247
|
+
elif self.strategy == StorageStrategy.GCS:
|
|
248
|
+
if not self.gcs:
|
|
249
|
+
raise ValueError("GCS settings are required for GCS strategy")
|
|
250
|
+
elif self.strategy == StorageStrategy.AZURE:
|
|
251
|
+
if not self.azure:
|
|
252
|
+
raise ValueError("Azure settings are required for AZURE strategy")
|
|
253
|
+
elif self.strategy == StorageStrategy.MINIO:
|
|
254
|
+
if not self.minio:
|
|
255
|
+
raise ValueError("MinIO settings are required for MINIO strategy")
|
|
256
|
+
return self
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class Config(BaseModel):
|
|
260
|
+
"""Top-level configuration container used by the whole application."""
|
|
261
|
+
logging: LoggingSettings
|
|
262
|
+
storage: StorageSettings
|
|
263
|
+
api_key: Optional[str] = Field(
|
|
264
|
+
default=None,
|
|
265
|
+
description="API key for authenticating incoming requests. None means no auth.",
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
@staticmethod
|
|
269
|
+
def _parse_bool(value: Optional[str]) -> bool:
|
|
270
|
+
"""Interpret common truthy representations used in env vars."""
|
|
271
|
+
if value is None:
|
|
272
|
+
return False
|
|
273
|
+
return value.strip().lower() in {"1", "true", "yes", "y", "on"}
|
|
274
|
+
|
|
275
|
+
@classmethod
|
|
276
|
+
def from_env(cls) -> "Config":
|
|
277
|
+
"""Construct Config from environment variables with sensible defaults and validation.
|
|
278
|
+
|
|
279
|
+
This does not configure logging by itself; see configure_logging().
|
|
280
|
+
"""
|
|
281
|
+
# Logging: only use DEBUG env var (truthy -> DEBUG, falsy -> INFO)
|
|
282
|
+
debug = cls._parse_bool(os.environ.get("DEBUG"))
|
|
283
|
+
logging_settings = LoggingSettings(debug=debug)
|
|
284
|
+
|
|
285
|
+
# Storage
|
|
286
|
+
raw_strategy = (os.environ.get("UPLOAD_STRATEGY", "LOCAL")).upper()
|
|
287
|
+
strategy = raw_strategy if raw_strategy in {e.value for e in StorageStrategy} else "LOCAL"
|
|
288
|
+
|
|
289
|
+
# Signed URL expiry (fallback to 3600 on invalid input)
|
|
290
|
+
try:
|
|
291
|
+
expires_in = int(os.environ.get("SIGNED_URL_EXPIRES_IN", "3600"))
|
|
292
|
+
if expires_in <= 0:
|
|
293
|
+
raise ValueError
|
|
294
|
+
except ValueError:
|
|
295
|
+
expires_in = 3600
|
|
296
|
+
|
|
297
|
+
# Strategy-specific settings (only populate the relevant one)
|
|
298
|
+
s3_settings = None
|
|
299
|
+
gcs_settings = None
|
|
300
|
+
azure_settings = None
|
|
301
|
+
minio_settings = None
|
|
302
|
+
|
|
303
|
+
if strategy == StorageStrategy.S3.value:
|
|
304
|
+
s3_settings = S3Settings(
|
|
305
|
+
access_key=os.environ.get("AWS_ACCESS_KEY") or None,
|
|
306
|
+
secret_key=os.environ.get("AWS_SECRET_ACCESS_KEY") or None,
|
|
307
|
+
region=os.environ.get("AWS_REGION") or None,
|
|
308
|
+
bucket=os.environ.get("S3_BUCKET", ""),
|
|
309
|
+
)
|
|
310
|
+
elif strategy == StorageStrategy.GCS.value:
|
|
311
|
+
gcs_settings = GCSSettings(
|
|
312
|
+
bucket=os.environ.get("GCS_BUCKET", ""),
|
|
313
|
+
credentials_path=os.environ.get("GCS_CREDENTIALS_PATH") or None,
|
|
314
|
+
)
|
|
315
|
+
elif strategy == StorageStrategy.AZURE.value:
|
|
316
|
+
azure_settings = AzureSettings(
|
|
317
|
+
account_name=os.environ.get("AZURE_STORAGE_ACCOUNT_NAME", ""),
|
|
318
|
+
account_key=os.environ.get("AZURE_STORAGE_ACCOUNT_KEY", ""),
|
|
319
|
+
container=os.environ.get("AZURE_CONTAINER", ""),
|
|
320
|
+
endpoint=os.environ.get("AZURE_BLOB_ENDPOINT"),
|
|
321
|
+
)
|
|
322
|
+
elif strategy == StorageStrategy.MINIO.value:
|
|
323
|
+
minio_settings = MinioSettings(
|
|
324
|
+
endpoint=os.environ.get("MINIO_ENDPOINT", ""),
|
|
325
|
+
access_key=os.environ.get("MINIO_ACCESS_KEY", ""),
|
|
326
|
+
secret_key=os.environ.get("MINIO_SECRET_KEY", ""),
|
|
327
|
+
bucket=os.environ.get("MINIO_BUCKET", ""),
|
|
328
|
+
region=os.environ.get("MINIO_REGION", "us-east-1") or "us-east-1",
|
|
329
|
+
verify_ssl=cls._parse_bool(os.environ.get("MINIO_VERIFY_SSL", "true")),
|
|
330
|
+
path_style=cls._parse_bool(os.environ.get("MINIO_PATH_STYLE", "true")),
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
storage_settings = StorageSettings(
|
|
334
|
+
strategy=StorageStrategy(strategy),
|
|
335
|
+
signed_url_expires_in=expires_in,
|
|
336
|
+
s3=s3_settings,
|
|
337
|
+
gcs=gcs_settings,
|
|
338
|
+
azure=azure_settings,
|
|
339
|
+
minio=minio_settings,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
# API key authentication (optional – empty/missing means no auth)
|
|
343
|
+
raw_api_key = (os.environ.get("API_KEY") or "").strip() or None
|
|
344
|
+
|
|
345
|
+
try:
|
|
346
|
+
return cls(logging=logging_settings, storage=storage_settings, api_key=raw_api_key)
|
|
347
|
+
except ValidationError as e:
|
|
348
|
+
# Wrap Pydantic validation errors in a simpler exception for callers
|
|
349
|
+
raise ValueError(f"Invalid configuration: {e}")
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
# Singleton instance and guard for one-time logging configuration
|
|
353
|
+
_CONFIG: Optional[Config] = None
|
|
354
|
+
_LOGGING_CONFIGURED: bool = False
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def configure_logging(config: Config) -> None:
|
|
358
|
+
"""Configure root logger format and level exactly once.
|
|
359
|
+
|
|
360
|
+
- Uses a more verbose format (file:line) in DEBUG level.
|
|
361
|
+
- Keeps concise formatting otherwise.
|
|
362
|
+
"""
|
|
363
|
+
global _LOGGING_CONFIGURED
|
|
364
|
+
if _LOGGING_CONFIGURED:
|
|
365
|
+
return
|
|
366
|
+
|
|
367
|
+
level = config.logging.level_no
|
|
368
|
+
root = logging.getLogger()
|
|
369
|
+
if not root.handlers:
|
|
370
|
+
handler = logging.StreamHandler()
|
|
371
|
+
# Use the debug flag (simplified API)
|
|
372
|
+
if config.logging.debug:
|
|
373
|
+
fmt = "%(asctime)s | %(levelname)s | %(name)s:%(lineno)d | %(message)s"
|
|
374
|
+
else:
|
|
375
|
+
fmt = "%(asctime)s | %(levelname)s | %(name)s | %(message)s"
|
|
376
|
+
handler.setFormatter(logging.Formatter(fmt))
|
|
377
|
+
root.addHandler(handler)
|
|
378
|
+
root.setLevel(level)
|
|
379
|
+
_LOGGING_CONFIGURED = True
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def get_config() -> Config:
|
|
383
|
+
"""Return the process-wide Config singleton and ensure logging is configured."""
|
|
384
|
+
global _CONFIG
|
|
385
|
+
if _CONFIG is None:
|
|
386
|
+
cfg = Config.from_env()
|
|
387
|
+
configure_logging(cfg)
|
|
388
|
+
_CONFIG = cfg
|
|
389
|
+
return _CONFIG
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: docforge-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for complete Office document manipulation — create, read, edit, convert DOCX/XLSX/PPTX/PDF/EML
|
|
5
|
+
Project-URL: Homepage, https://github.com/filhocf/docforge-mcp
|
|
6
|
+
Project-URL: Repository, https://github.com/filhocf/docforge-mcp
|
|
7
|
+
Project-URL: Issues, https://github.com/filhocf/docforge-mcp/issues
|
|
8
|
+
Author-email: Claudio Ferreira Filho <filhocf@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: docforge,documents,docx,mcp,office,pdf,pptx,xlsx
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: beautifulsoup4>=4.13.4
|
|
22
|
+
Requires-Dist: defusedxml>=0.7.1
|
|
23
|
+
Requires-Dist: fastmcp>=3.2.0
|
|
24
|
+
Requires-Dist: fpdf2>=2.8.0
|
|
25
|
+
Requires-Dist: markdown>=3.5
|
|
26
|
+
Requires-Dist: openpyxl>=3.1.2
|
|
27
|
+
Requires-Dist: pydantic>=2.11.5
|
|
28
|
+
Requires-Dist: pystache>=0.6.5
|
|
29
|
+
Requires-Dist: python-docx==1.2.0
|
|
30
|
+
Requires-Dist: python-dotenv>=1.2.1
|
|
31
|
+
Requires-Dist: python-pptx==1.0.2
|
|
32
|
+
Requires-Dist: pyyaml
|
|
33
|
+
Requires-Dist: requests>=2.31.0
|
|
34
|
+
Provides-Extra: cloud
|
|
35
|
+
Requires-Dist: azure-storage-blob>=12.23.1; extra == 'cloud'
|
|
36
|
+
Requires-Dist: boto3>=1.40.1; extra == 'cloud'
|
|
37
|
+
Requires-Dist: botocore>=1.40.1; extra == 'cloud'
|
|
38
|
+
Requires-Dist: google-cloud-storage>=2.18.2; extra == 'cloud'
|
|
39
|
+
Provides-Extra: dev
|
|
40
|
+
Requires-Dist: pytest-asyncio>=1.0; extra == 'dev'
|
|
41
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
42
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
43
|
+
Requires-Dist: ruff>=0.4.0; extra == 'dev'
|
|
44
|
+
Description-Content-Type: text/markdown
|
|
45
|
+
|
|
46
|
+
# docforge-mcp
|
|
47
|
+
|
|
48
|
+
MCP server for **complete Office document manipulation** — create, read, edit, convert, and template DOCX, XLSX, PPTX, PDF, and EML files.
|
|
49
|
+
|
|
50
|
+
Built for AI agents that need full document lifecycle control, not just one-shot generation.
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# Via uvx (no install needed)
|
|
56
|
+
uvx docforge-mcp
|
|
57
|
+
|
|
58
|
+
# Or install globally
|
|
59
|
+
uv tool install docforge-mcp
|
|
60
|
+
|
|
61
|
+
# Or pip
|
|
62
|
+
pip install docforge-mcp
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Tools (38)
|
|
66
|
+
|
|
67
|
+
| Category | Tools | Capabilities |
|
|
68
|
+
|----------|:-----:|--------------|
|
|
69
|
+
| **Word (DOCX)** | 12 | Create from markdown, read, edit paragraphs, insert, delete, search/replace, headers/footers, images, merge, templates |
|
|
70
|
+
| **Excel (XLSX)** | 7 | Create from markdown, read sheets, edit cells, insert/delete rows, charts, conditional formatting |
|
|
71
|
+
| **PowerPoint (PPTX)** | 9 | Create presentations, read slides, edit text, add shapes/images, reorder, duplicate, delete, merge, templates |
|
|
72
|
+
| **PDF** | 3 | Create from markdown, convert DOCX→PDF, read |
|
|
73
|
+
| **Email (EML)** | 1 | Create HTML email drafts |
|
|
74
|
+
| **XML** | 1 | Create well-formed XML |
|
|
75
|
+
| **Templates** | 2 | Render DOCX/PPTX with variables, conditionals (`{{#if}}`), loops (`{{#each}}`) |
|
|
76
|
+
| **Metadata** | 1 | Get document info/stats |
|
|
77
|
+
| **Merge** | 2 | Merge multiple DOCX or PPTX files |
|
|
78
|
+
|
|
79
|
+
## Usage
|
|
80
|
+
|
|
81
|
+
### As MCP server (stdio — default)
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
docforge-mcp
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### As HTTP server
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
MCP_TRANSPORT=streamable-http MCP_PORT=8958 docforge-mcp
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### MCP client configuration
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"mcpServers": {
|
|
98
|
+
"office-documents": {
|
|
99
|
+
"command": "docforge-mcp",
|
|
100
|
+
"autoApprove": ["read_document", "get_document_info", "get_docx_paragraphs", "get_pptx_slides", "get_xlsx_sheets"]
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Origins
|
|
107
|
+
|
|
108
|
+
This project was born from [ForLegalAI/mcp-ms-office-documents](https://github.com/ForLegalAI/mcp-ms-office-documents) (MIT license). It diverged in scope and philosophy:
|
|
109
|
+
|
|
110
|
+
| | ForLegalAI (upstream) | docforge-mcp |
|
|
111
|
+
|---|---|---|
|
|
112
|
+
| **Goal** | One-shot document generation | Full document lifecycle |
|
|
113
|
+
| **Read** | ❌ | ✅ Read any DOCX/XLSX/PPTX |
|
|
114
|
+
| **Edit** | ❌ | ✅ Edit paragraphs, cells, slides |
|
|
115
|
+
| **Convert** | ❌ | ✅ DOCX→PDF |
|
|
116
|
+
| **Templates** | Simple `{{var}}` | Conditionals + loops |
|
|
117
|
+
| **Transport** | Docker + HTTP only | stdio + HTTP |
|
|
118
|
+
| **Install** | Docker | `uvx docforge-mcp` |
|
|
119
|
+
|
|
120
|
+
We continue to contribute compatible features upstream (PRs #57, #58, #59) while developing the full toolkit independently.
|
|
121
|
+
|
|
122
|
+
## Development
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
git clone https://github.com/filhocf/docforge-mcp.git
|
|
126
|
+
cd docforge-mcp
|
|
127
|
+
uv sync --group dev
|
|
128
|
+
uv run pytest tests/ -v
|
|
129
|
+
uv run ruff check .
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
MIT — see [LICENSE](LICENSE) for details. Original work © ForLegalAI, extensions © Claudio Ferreira Filho.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
docx_tools/__init__.py,sha256=-tB13zkY7mq6uC1vQ2y8VRS3MKFnuZ2P4c40R6OoGXs,191
|
|
2
|
+
docx_tools/base_docx_tool.py,sha256=JfoHBHDjFlmXpz5vsyoFGeS5fLt6cDKW9ADNBHMUMk8,8105
|
|
3
|
+
docx_tools/conditional_templates.py,sha256=J8tOVqtalDgdNASlhuiXSn1BK2sXkx5IiDimuvLU-7M,3854
|
|
4
|
+
docx_tools/dynamic_docx_tools.py,sha256=orRP9qf0szpq7JZKdw_t2cbXdhf98Q52Te22z9D5aKc,19370
|
|
5
|
+
docx_tools/helpers.py,sha256=LHk4gadajWB3v31sVfRyfccXgD04YLvwjsGL4X8j1Q8,24987
|
|
6
|
+
docx_tools/advanced/__init__.py,sha256=m98EfvjCxec94C1TSUCAYA5vjrhLU6DAINUOl3E5lB4,413
|
|
7
|
+
docx_tools/advanced/features.py,sha256=yPDAhEmYh1GuhrVyIwiuiwROvZmPyTRSD_GSdoRUXJk,3933
|
|
8
|
+
edit_tools/__init__.py,sha256=85zs1dL3jYzmPDtkQukga__G3eeEIzww7tftqECJ2WQ,641
|
|
9
|
+
edit_tools/docx_editor.py,sha256=wh1mj19laV-Icma-G2JJqJgoO0bQO4zaPs7BhedSu3U,3327
|
|
10
|
+
edit_tools/pptx_editor.py,sha256=oo90BzFmGKgbza3eqi_jNo3rW5caAB-dgSzhqwHIHbo,2335
|
|
11
|
+
edit_tools/xlsx_editor.py,sha256=ejynFl4bOFlNll3TN3j7Ajuyx1EWXdsjbH3LrUTtGBY,2000
|
|
12
|
+
email_tools/__init__.py,sha256=wXJ38twMrLdj0u2fHqL_7RrxzkywTZwoWOs3vjWg6wQ,316
|
|
13
|
+
email_tools/base_email_tool.py,sha256=JcoshRKE5QDC-53YgvACejRKt6WA6_UsnZx3b0AhfEA,4042
|
|
14
|
+
email_tools/dynamic_email_tools.py,sha256=Ood8DXdbjNQfLuBaDVOxfb4TAHN2GPY5zz7KUD0qa78,8695
|
|
15
|
+
mcp_office_documents/__init__.py,sha256=4-NTLmaHjZItY3qtL49ouIt5CJgbZj6TZzclgPEWWMc,109
|
|
16
|
+
mcp_office_documents/app.py,sha256=9lxBo6028a2dbzVxcat58UYohGMx1hP__aSOMDk96vs,42102
|
|
17
|
+
mcp_office_documents/server.py,sha256=7aUFw5q12g8TYPoru-Q0e1W_wG9ngoF-6yMM8kNbke8,1023
|
|
18
|
+
merge_tools/__init__.py,sha256=GB6fXZmzA66njxy2kEErzqEElnNdovLjHuWgqAFNXvE,152
|
|
19
|
+
merge_tools/merger.py,sha256=hpSCI5nO09tkaStVr_td6wSXHGMlgrLbDwrGA2MCW-k,1513
|
|
20
|
+
pdf_tools/__init__.py,sha256=o5sS-o-cct-CW4MhgAYRsXN1UGD6_NxiwgJKhC55_OA,182
|
|
21
|
+
pdf_tools/base_pdf_tool.py,sha256=kh3xrvWtsEa9e-_LmSEetBM2W8ufjTjByZqCjNcUKLU,4557
|
|
22
|
+
pptx_tools/__init__.py,sha256=D02b_ynI68WqoPfiT4bD52AunSMCZlMznbckEfj9vMA,474
|
|
23
|
+
pptx_tools/base_pptx_tool.py,sha256=egLH6e5vQXWb4NTkznksuMG9k3JIAKc7fNI1Vxtk-3A,1350
|
|
24
|
+
pptx_tools/chart_utils.py,sha256=7ehwIdMFXzdq-gVdPmHYXOjU-THlyD_cHMBqCekMrb8,5495
|
|
25
|
+
pptx_tools/constants.py,sha256=KkuSAAADPvQTtYNcs6YluKEaD-D3cHMG9yrty7riXC0,1757
|
|
26
|
+
pptx_tools/helpers.py,sha256=MUPFT5dwGQpeyvPd_0n5VaDF71je9ZEvFeXkCcnofPo,14276
|
|
27
|
+
pptx_tools/image_utils.py,sha256=_m06rT14_pvfq_bK4RglU5IS0UrA8M0fuhhieUHpIVA,6805
|
|
28
|
+
pptx_tools/slide_builder.py,sha256=NacHw2oAIHcNwiy7bB51xgu4romiTHoFfgbYqe3KBjo,14372
|
|
29
|
+
pptx_tools/advanced/__init__.py,sha256=I7TBtGEpQ1euvNE63o2tYTlie_AC5uYOY7Y3oOUJhaQ,316
|
|
30
|
+
pptx_tools/advanced/features.py,sha256=0m5paFh5D5AIAnB4dvzfG1J5Wt3TLPt6Dztj8AWGGI4,3962
|
|
31
|
+
read_tools/__init__.py,sha256=tz0Ki1LeO_yeHuySUzU-rykMx_sYTOsbHyqbesMSs84,520
|
|
32
|
+
read_tools/docx_reader.py,sha256=j-skoYnSq0u_CagJ9mkF69TiU49uH8ojJjlBOqVBNPg,1820
|
|
33
|
+
read_tools/pptx_reader.py,sha256=n17xGH9twv85LNxmjQiOT-GHTbOwrvKtAvlNyak8Yh0,2030
|
|
34
|
+
read_tools/xlsx_reader.py,sha256=PB0D4xOuuDSeyOyX_yrprwr2XECF3bZi9Z84UwzL93Y,2060
|
|
35
|
+
upload_tools/__init__.py,sha256=3rOD2I4grxO5NiSWfX-irLZUCk6ccYMgpjR3IM-08nY,58
|
|
36
|
+
upload_tools/main.py,sha256=L_0g4MuwlROHHXk0GZA1HWEafu-F-fD3I2HrZg9KSpU,3337
|
|
37
|
+
upload_tools/utils.py,sha256=VxssNuVjH6fMH29tzEhyVY6uR_ZyoeNReVD13jAssnU,2130
|
|
38
|
+
upload_tools/backends/__init__.py,sha256=GCpbbMocEO_KyhFMWqJIHO7jOobFKdIhYa-BXaUXwqM,304
|
|
39
|
+
upload_tools/backends/azure.py,sha256=G_a0iAJjj5KzGhc5vydBa-9SUU5M3ov2NN_OD_PaFD4,2359
|
|
40
|
+
upload_tools/backends/gcs.py,sha256=ZNYvh-mCQ6TacS3-tfSrsSWTuacTudZ0uMgdgsdsD_s,3096
|
|
41
|
+
upload_tools/backends/local.py,sha256=YmVzfqYqAZawl1ldBljb0KQ3M-1fGLjlX8uvvbEZUqw,936
|
|
42
|
+
upload_tools/backends/minio.py,sha256=y6302AB5eAE5ly_6C5N4MwJ2Bp4u8BeqUuWW0cPbf-c,2188
|
|
43
|
+
upload_tools/backends/s3.py,sha256=9oM1VzqrnxGEoWxud-ptlaVS_39XxfwnPG2UePls8MI,5115
|
|
44
|
+
xlsx_tools/__init__.py,sha256=92N2lwAHpOTpnLGaPpeVAPRaDr2tMeAZLsX1Ej_E5Zo,89
|
|
45
|
+
xlsx_tools/base_xlsx_tool.py,sha256=283wJj8poArHZlbTAhPangm2urvvXrnS5HsjdWkB1AU,7347
|
|
46
|
+
xlsx_tools/formatting.py,sha256=QGGEPTYfl0TiUt9zTHtXyNcSEpnzdJYNx9o5z2Um0FY,3080
|
|
47
|
+
xlsx_tools/helpers.py,sha256=uCyk7T1URRdVIrLjJfCQIzrYNKiXkxB9PqpMAljSS9o,15306
|
|
48
|
+
xlsx_tools/charts/__init__.py,sha256=XX_cc19dEyFy69NWIpGyUBPPC7oWMPuiOWfdxskfjFU,143
|
|
49
|
+
xlsx_tools/charts/chart_builder.py,sha256=AajtOYVIZeaGTUNVXY0436pbkAdMcFcYe2z8UyFhXqA,3622
|
|
50
|
+
xml_tools/__init__.py,sha256=w2Mo7__yNxn9WnAwdbjgARJw3P4dbrIElxUJz99VK3g,163
|
|
51
|
+
xml_tools/base_xml_tool.py,sha256=3Spnta4wXD82_-T-HdR4BBEdums9M5h_BFTUDi_wVaA,3776
|
|
52
|
+
config.py,sha256=BIZns95Jhv2_ko2HRXd6VZsUFSx7ICPf43jCF80LH1w,15188
|
|
53
|
+
middleware.py,sha256=0aj86-CNbwvW2XyDFxpac89qFONNE4DioUiMJZYCETo,4112
|
|
54
|
+
template_utils.py,sha256=zetBJiODceS_MIv_Sfxpc2BPSQQc_TpBhJZa4RsR5LQ,4045
|
|
55
|
+
docforge_mcp-0.1.0.dist-info/METADATA,sha256=Y1XiVbRYyDqSt5waYXgslOmkSZBrGwpfjTZHSeh_vfg,4562
|
|
56
|
+
docforge_mcp-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
57
|
+
docforge_mcp-0.1.0.dist-info/entry_points.txt,sha256=fHSUxIS7vhLHvsttaz0gvIS5l1Km6V6GdI6echcuh5Q,125
|
|
58
|
+
docforge_mcp-0.1.0.dist-info/licenses/LICENSE,sha256=5FW67cyw2o5VQGMkPQucnpbbuHfwbKxcxbiRptyRJZY,1109
|
|
59
|
+
docforge_mcp-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ForLegalAI
|
|
4
|
+
Copyright (c) 2026 Claudio Ferreira Filho
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
docx_tools/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Advanced DOCX features: images, headers/footers, layout, lists."""
|
|
2
|
+
|
|
3
|
+
from docx_tools.advanced.features import (
|
|
4
|
+
add_bullet_list,
|
|
5
|
+
add_header_footer,
|
|
6
|
+
add_image_to_docx,
|
|
7
|
+
add_numbered_list,
|
|
8
|
+
merge_table_cells,
|
|
9
|
+
set_page_margins,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"add_image_to_docx",
|
|
14
|
+
"add_header_footer",
|
|
15
|
+
"set_page_margins",
|
|
16
|
+
"add_bullet_list",
|
|
17
|
+
"add_numbered_list",
|
|
18
|
+
"merge_table_cells",
|
|
19
|
+
]
|