MCP (Model Context Protocol) server built with FastMCP 3.0 that exposes Office document generation as MCP tools. Runs as a Docker container (Python 3.12, Alpine) on port 8958 at /mcp using streamable-HTTP transport. Entry point: main.py.
main.py ← Registers all MCP tools on a single FastMCP instance
├── {docx,xlsx,pptx,email,xml}_tools/
│ ├── __init__.py ← Re-exports the public function (e.g. markdown_to_word)
│ ├── base_*_tool.py ← Core conversion logic (markdown → document bytes)
│ ├── helpers.py ← Parsing, formatting, shared utilities
│ └── dynamic_*_tools.py ← YAML-driven tool registration (docx, email only)
├── upload_tools/
│ ├── main.py ← upload_file() dispatches to strategy backend
│ └── backends/{local,s3,gcs,azure,minio}.py
├── config.py ← Singleton Config from env vars (Pydantic v2), logging setup
├── template_utils.py ← Template resolution: custom_templates/ → default_templates/
└── middleware.py ← Optional API key auth (Bearer / x-api-key header)
Data flow: Every tool converts input → in-memory bytes → calls upload_file(file_obj, suffix) → backend saves/uploads → returns URL or path string to the MCP client.
- Config is centralized in
config.py— no module readsos.environdirectly. Access viaget_config()singleton. - Template resolution (
template_utils.py): searchescustom_templates/beforedefault_templates/, with/app/*container paths tried first, then local paths. Never hardcode template paths. - Dynamic tool registration: YAML files in
config/define parameterized email/docx templates. Each YAML entry becomes a separate MCP tool at startup viaregister_*_template_tools_from_yaml(mcp, path). Placeholders use Mustache syntax{{name}}. Seeconfig/docx_templates.yamlfor the canonical example. - Pydantic models for tool arguments are created dynamically with
create_model()indynamic_*_tools.py. TheTYPE_MAPdict maps YAML type strings to Python types. - Error handling in tools: raise
fastmcp.exceptions.ToolErrorfor user-facing errors; useRuntimeErrorin upload/backend layers. - Logging: use
logging.getLogger(__name__)everywhere. Level controlled byDEBUGenv var only.
- Create
<type>_tools/package with__init__.py,base_<type>_tool.py, and optionalhelpers.py. - The base tool function should: accept content → produce an
io.BytesIO→ callupload_file(buffer, "<ext>")→ return the result string. - Register the async wrapper in
main.pyusing@mcp.tool(name=..., description=..., tags=..., annotations=...). - Use
Annotated[<type>, Field(description=...)]for all tool parameters — the descriptions are critical because MCP clients (AI models) rely on them.
pytest # Run all tests (asyncio_mode=auto in pytest.ini)
pytest tests/test_docx_base.py # Single module- Tests live in
tests/and output generated files totests/output/{docx,pptx,xlsx}/for manual inspection. - Upload is mocked in tests — patch
upload_fileor the specific*_tool.upload_fileto capture bytes without needing a real backend. Seetest_xlsx_creation.py::_create_workbook_from_markdownfor the pattern. - PPTX tests instantiate
PowerpointPresentationdirectly and call.save()to get a buffer, bypassing upload entirely. - No
.envrequired for tests —config.pydefaults toLOCALstrategy and INFO logging.