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.
Files changed (59) hide show
  1. config.py +389 -0
  2. docforge_mcp-0.1.0.dist-info/METADATA +134 -0
  3. docforge_mcp-0.1.0.dist-info/RECORD +59 -0
  4. docforge_mcp-0.1.0.dist-info/WHEEL +4 -0
  5. docforge_mcp-0.1.0.dist-info/entry_points.txt +3 -0
  6. docforge_mcp-0.1.0.dist-info/licenses/LICENSE +22 -0
  7. docx_tools/__init__.py +5 -0
  8. docx_tools/advanced/__init__.py +19 -0
  9. docx_tools/advanced/features.py +101 -0
  10. docx_tools/base_docx_tool.py +226 -0
  11. docx_tools/conditional_templates.py +112 -0
  12. docx_tools/dynamic_docx_tools.py +539 -0
  13. docx_tools/helpers.py +703 -0
  14. edit_tools/__init__.py +16 -0
  15. edit_tools/docx_editor.py +94 -0
  16. edit_tools/pptx_editor.py +60 -0
  17. edit_tools/xlsx_editor.py +53 -0
  18. email_tools/__init__.py +10 -0
  19. email_tools/base_email_tool.py +114 -0
  20. email_tools/dynamic_email_tools.py +177 -0
  21. mcp_office_documents/__init__.py +3 -0
  22. mcp_office_documents/app.py +826 -0
  23. mcp_office_documents/server.py +40 -0
  24. merge_tools/__init__.py +5 -0
  25. merge_tools/merger.py +45 -0
  26. middleware.py +110 -0
  27. pdf_tools/__init__.py +5 -0
  28. pdf_tools/base_pdf_tool.py +131 -0
  29. pptx_tools/__init__.py +15 -0
  30. pptx_tools/advanced/__init__.py +10 -0
  31. pptx_tools/advanced/features.py +97 -0
  32. pptx_tools/base_pptx_tool.py +44 -0
  33. pptx_tools/chart_utils.py +177 -0
  34. pptx_tools/constants.py +52 -0
  35. pptx_tools/helpers.py +459 -0
  36. pptx_tools/image_utils.py +227 -0
  37. pptx_tools/slide_builder.py +398 -0
  38. read_tools/__init__.py +11 -0
  39. read_tools/docx_reader.py +63 -0
  40. read_tools/pptx_reader.py +62 -0
  41. read_tools/xlsx_reader.py +65 -0
  42. template_utils.py +133 -0
  43. upload_tools/__init__.py +4 -0
  44. upload_tools/backends/__init__.py +13 -0
  45. upload_tools/backends/azure.py +66 -0
  46. upload_tools/backends/gcs.py +75 -0
  47. upload_tools/backends/local.py +28 -0
  48. upload_tools/backends/minio.py +59 -0
  49. upload_tools/backends/s3.py +139 -0
  50. upload_tools/main.py +79 -0
  51. upload_tools/utils.py +60 -0
  52. xlsx_tools/__init__.py +3 -0
  53. xlsx_tools/base_xlsx_tool.py +212 -0
  54. xlsx_tools/charts/__init__.py +5 -0
  55. xlsx_tools/charts/chart_builder.py +113 -0
  56. xlsx_tools/formatting.py +86 -0
  57. xlsx_tools/helpers.py +383 -0
  58. xml_tools/__init__.py +4 -0
  59. 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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ docforge-mcp = mcp_office_documents.server:main
3
+ mcp-ms-office-documents = mcp_office_documents.server:main
@@ -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,5 @@
1
+ from .base_docx_tool import markdown_to_word
2
+ from .dynamic_docx_tools import register_docx_template_tools_from_yaml
3
+
4
+ __all__ = ["markdown_to_word", "register_docx_template_tools_from_yaml"]
5
+
@@ -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
+ ]