├── .env.example ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── SECURITY.md ├── codecov.yml ├── mergify.yml ├── scripts │ └── release.sh └── workflows │ ├── cd.yml │ ├── ci.yml │ ├── job-build.yml │ ├── job-checks.yml │ └── pypi.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .python-version ├── CHANGELOG.md ├── CITATION.cff ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── MAINTAINERS.md ├── README.md ├── clients └── test_llama_stack.py ├── docling_mcp ├── __init__.py ├── docling_cache.py ├── docling_settings.py ├── logger.py ├── servers │ └── mcp_server.py ├── shared.py └── tools │ ├── __init__.py │ ├── applications.py │ ├── conversion.py │ └── generation.py ├── docs └── integrations │ └── claude_desktop_config.json ├── pyproject.toml ├── tests ├── __init__.py └── test_generation_tools.py └── uv.lock /.env.example: -------------------------------------------------------------------------------- 1 | RAG_ENABLED=true 2 | OLLAMA_MODEL=granite3.2:latest 3 | EMBEDDING_MODEL=BAAI/bge-base-en-v1.5 -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security and Disclosure Information Policy for the Docling Project 2 | 3 | The Docling team and community take security bugs seriously. We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions. 4 | 5 | ## Reporting a Vulnerability 6 | 7 | If you think you've identified a security issue in an Docling project repository, please DO NOT report the issue publicly via the GitHub issue tracker, etc. 8 | 9 | Instead, send an email with as many details as possible to [deepsearch-core@zurich.ibm.com](mailto:deepsearch-core@zurich.ibm.com). This is a private mailing list for the maintainers team. 10 | 11 | Please do not create a public issue. 12 | 13 | ## Security Vulnerability Response 14 | 15 | Each report is acknowledged and analyzed by the core maintainers within 3 working days. 16 | 17 | Any vulnerability information shared with core maintainers stays within the Docling project and will not be disseminated to other projects unless it is necessary to get the issue fixed. 18 | 19 | After the initial reply to your report, the security team will keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance. 20 | 21 | ## Security Alerts 22 | 23 | We will send announcements of security vulnerabilities and steps to remediate on the [Docling announcements](https://github.com/DS4SD/docling/discussions/categories/announcements). 24 | -------------------------------------------------------------------------------- /.github/codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | # https://docs.codecov.io/docs/comparing-commits 3 | allow_coverage_offsets: true 4 | coverage: 5 | status: 6 | project: 7 | default: 8 | informational: true 9 | target: auto # auto compares coverage to the previous base commit 10 | if_ci_failed: success 11 | flags: 12 | - docling_mcp 13 | comment: 14 | layout: "reach, diff, flags, files" 15 | behavior: default 16 | require_changes: false # if true: only post the comment if coverage changes 17 | branches: # branch names that can post comment 18 | - "main" 19 | -------------------------------------------------------------------------------- /.github/mergify.yml: -------------------------------------------------------------------------------- 1 | merge_protections: 2 | - name: Enforce conventional commit 3 | description: Make sure that we follow https://www.conventionalcommits.org/en/v1.0.0/ 4 | if: 5 | - base = main 6 | success_conditions: 7 | - "title ~= 8 | ^(fix|feat|docs|style|refactor|perf|test|build|ci|chore|revert)(?:\\(.+\ 9 | \\))?(!)?:" 10 | -------------------------------------------------------------------------------- /.github/scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e # trigger failure on error - do not remove! 4 | set -x # display command on output 5 | 6 | if [ -z "${TARGET_VERSION}" ]; then 7 | >&2 echo "No TARGET_VERSION specified" 8 | exit 1 9 | fi 10 | CHGLOG_FILE="${CHGLOG_FILE:-CHANGELOG.md}" 11 | 12 | # update package version 13 | uvx --from=toml-cli toml set --toml-path=pyproject.toml project.version "${TARGET_VERSION}" 14 | uv lock --upgrade-package docling-mcp 15 | 16 | # collect release notes 17 | REL_NOTES=$(mktemp) 18 | uv run --no-sync semantic-release changelog --unreleased >> "${REL_NOTES}" 19 | 20 | # update changelog 21 | TMP_CHGLOG=$(mktemp) 22 | TARGET_TAG_NAME="v${TARGET_VERSION}" 23 | RELEASE_URL="$(gh repo view --json url -q ".url")/releases/tag/${TARGET_TAG_NAME}" 24 | printf "## [${TARGET_TAG_NAME}](${RELEASE_URL}) - $(date -Idate)\n\n" >> "${TMP_CHGLOG}" 25 | cat "${REL_NOTES}" >> "${TMP_CHGLOG}" 26 | if [ -f "${CHGLOG_FILE}" ]; then 27 | printf "\n" | cat - "${CHGLOG_FILE}" >> "${TMP_CHGLOG}" 28 | fi 29 | mv "${TMP_CHGLOG}" "${CHGLOG_FILE}" 30 | 31 | # push changes 32 | git config --global user.name 'github-actions[bot]' 33 | git config --global user.email 'github-actions[bot]@users.noreply.github.com' 34 | git add pyproject.toml uv.lock "${CHGLOG_FILE}" 35 | COMMIT_MSG="chore: bump version to ${TARGET_VERSION} [skip ci]" 36 | git commit -m "${COMMIT_MSG}" 37 | git push origin main 38 | 39 | # create GitHub release (incl. Git tag) 40 | gh release create "${TARGET_TAG_NAME}" -F "${REL_NOTES}" 41 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: "Run CD" 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | code-checks: 8 | uses: ./.github/workflows/job-checks.yml 9 | pre-release-check: 10 | runs-on: ubuntu-latest 11 | outputs: 12 | TARGET_TAG_V: ${{ steps.version_check.outputs.TRGT_VERSION }} 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 # for fetching tags, required for semantic-release 17 | - name: Install uv and set the python version 18 | uses: astral-sh/setup-uv@v5 19 | with: 20 | enable-cache: true 21 | - name: Install dependencies 22 | run: uv sync --only-dev 23 | - name: Check version of potential release 24 | id: version_check 25 | run: | 26 | TRGT_VERSION=$(uv run --no-sync semantic-release print-version) 27 | echo "TRGT_VERSION=${TRGT_VERSION}" >> "$GITHUB_OUTPUT" 28 | echo "${TRGT_VERSION}" 29 | - name: Check notes of potential release 30 | run: uv run --no-sync semantic-release changelog --unreleased 31 | release: 32 | needs: [code-checks, pre-release-check] 33 | if: needs.pre-release-check.outputs.TARGET_TAG_V != '' 34 | environment: auto-release 35 | runs-on: ubuntu-latest 36 | concurrency: release 37 | steps: 38 | - uses: actions/create-github-app-token@v1 39 | id: app-token 40 | with: 41 | app-id: ${{ vars.CI_APP_ID }} 42 | private-key: ${{ secrets.CI_PRIVATE_KEY }} 43 | - uses: actions/checkout@v4 44 | with: 45 | token: ${{ steps.app-token.outputs.token }} 46 | fetch-depth: 0 # for fetching tags, required for semantic-release 47 | - name: Install uv and set the python version 48 | uses: astral-sh/setup-uv@v5 49 | with: 50 | enable-cache: true 51 | - name: Install dependencies 52 | run: uv sync --only-dev 53 | - name: Run release script 54 | env: 55 | GH_TOKEN: ${{ steps.app-token.outputs.token }} 56 | TARGET_VERSION: ${{ needs.pre-release-check.outputs.TARGET_TAG_V }} 57 | CHGLOG_FILE: CHANGELOG.md 58 | run: ./.github/scripts/release.sh 59 | shell: bash 60 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "Run CI" 2 | 3 | on: 4 | pull_request: 5 | types: [opened, reopened, synchronize] 6 | push: 7 | branches: 8 | - "**" 9 | - "!gh-pages" 10 | 11 | jobs: 12 | code-checks: 13 | if: ${{ github.event_name == 'push' || github.event.pull_request.head.repo.full_name != 'docling-project/docling-mcp' }} 14 | uses: ./.github/workflows/job-checks.yml 15 | secrets: 16 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 17 | -------------------------------------------------------------------------------- /.github/workflows/job-build.yml: -------------------------------------------------------------------------------- 1 | name: Run checks 2 | 3 | on: 4 | workflow_call: 5 | 6 | jobs: 7 | build-package: 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | python-version: ['3.12'] 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Install uv and set the python version 15 | uses: astral-sh/setup-uv@v5 16 | with: 17 | python-version: ${{ matrix.python-version }} 18 | enable-cache: true 19 | - name: Install dependencies 20 | run: uv sync --all-extras 21 | - name: Build package 22 | run: uv build 23 | - name: Check content of wheel 24 | run: unzip -l dist/*.whl 25 | - name: Store the distribution packages 26 | uses: actions/upload-artifact@v4 27 | with: 28 | name: python-package-distributions 29 | path: dist/ 30 | -------------------------------------------------------------------------------- /.github/workflows/job-checks.yml: -------------------------------------------------------------------------------- 1 | name: Run checks 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | push_coverage: 7 | type: boolean 8 | description: "If true, the coverage results are pushed to codecov.io." 9 | default: true 10 | secrets: 11 | CODECOV_TOKEN: 12 | required: false 13 | jobs: 14 | py-lint: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | python-version: ['3.10', '3.11', '3.12'] 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Install uv and set the python version 22 | uses: astral-sh/setup-uv@v5 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | enable-cache: true 26 | - name: pre-commit cache key 27 | run: echo "PY=$(python -VV | sha256sum | cut -d' ' -f1)" >> "$GITHUB_ENV" 28 | - uses: actions/cache@v4 29 | with: 30 | path: ~/.cache/pre-commit 31 | key: pre-commit|${{ env.PY }}|${{ hashFiles('.pre-commit-config.yaml') }} 32 | - name: Install dependencies 33 | run: uv sync --frozen --all-extras 34 | - name: Check style and run tests 35 | run: pre-commit run --all-files 36 | - name: Upload coverage to Codecov 37 | if: inputs.push_coverage 38 | uses: codecov/codecov-action@v5 39 | with: 40 | token: ${{ secrets.CODECOV_TOKEN }} 41 | files: ./coverage.xml 42 | 43 | build-package: 44 | uses: ./.github/workflows/job-build.yml 45 | 46 | test-package: 47 | needs: 48 | - build-package 49 | runs-on: ubuntu-latest 50 | strategy: 51 | matrix: 52 | python-version: ['3.12'] 53 | steps: 54 | - name: Download all the dists 55 | uses: actions/download-artifact@v4 56 | with: 57 | name: python-package-distributions 58 | path: dist/ 59 | - name: Install uv and set the python version 60 | uses: astral-sh/setup-uv@v5 61 | with: 62 | python-version: ${{ matrix.python-version }} 63 | enable-cache: true 64 | cache-dependency-glob: "" # avoid default file matching of uv.lock 65 | - name: Install package 66 | run: uv pip install dist/*.whl 67 | - name: Create the server 68 | run: python -c 'import docling_mcp.servers.mcp_server' 69 | -------------------------------------------------------------------------------- /.github/workflows/pypi.yml: -------------------------------------------------------------------------------- 1 | name: "Build and publish package" 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | 12 | build-package: 13 | uses: ./.github/workflows/job-build.yml 14 | 15 | build-and-publish: 16 | needs: 17 | - build-package 18 | runs-on: ubuntu-latest 19 | environment: 20 | name: pypi 21 | url: https://pypi.org/p/docling-mcp # Replace with your PyPI project name 22 | permissions: 23 | id-token: write # IMPORTANT: mandatory for trusted publishing 24 | steps: 25 | - name: Download all the dists 26 | uses: actions/download-artifact@v4 27 | with: 28 | name: python-package-distributions 29 | path: dist/ 30 | - name: Publish distribution 📦 to PyPI 31 | uses: pypa/gh-action-pypi-publish@release/v1 32 | with: 33 | # generating signed digital attestations currently not working with reusable workflows 34 | attestations: false 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | **.DS_Store 3 | _cache/** 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | cover/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | .pybuilder/ 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | # For a library or package, you might want to ignore these files since the code is 91 | # intended to run in multiple environments; otherwise, check them in: 92 | # .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 98 | # install all needed dependencies. 99 | #Pipfile.lock 100 | 101 | # UV 102 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 103 | # This is especially recommended for binary packages to ensure reproducibility, and is more 104 | # commonly ignored for libraries. 105 | #uv.lock 106 | 107 | # poetry 108 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 109 | # This is especially recommended for binary packages to ensure reproducibility, and is more 110 | # commonly ignored for libraries. 111 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 112 | #poetry.lock 113 | 114 | # pdm 115 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 116 | #pdm.lock 117 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 118 | # in version control. 119 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 120 | .pdm.toml 121 | .pdm-python 122 | .pdm-build/ 123 | 124 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 125 | __pypackages__/ 126 | 127 | # Celery stuff 128 | celerybeat-schedule 129 | celerybeat.pid 130 | 131 | # SageMath parsed files 132 | *.sage.py 133 | 134 | # Environments 135 | .env 136 | .venv 137 | env/ 138 | venv/ 139 | ENV/ 140 | env.bak/ 141 | venv.bak/ 142 | 143 | # Spyder project settings 144 | .spyderproject 145 | .spyproject 146 | 147 | # Rope project settings 148 | .ropeproject 149 | 150 | # mkdocs documentation 151 | /site 152 | 153 | # mypy 154 | .mypy_cache/ 155 | .dmypy.json 156 | dmypy.json 157 | 158 | # Pyre type checker 159 | .pyre/ 160 | 161 | # pytype static type analyzer 162 | .pytype/ 163 | 164 | # Cython debug symbols 165 | cython_debug/ 166 | 167 | # PyCharm 168 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 169 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 170 | # and can be added to the global gitignore or merged into this file. For a more nuclear 171 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 172 | #.idea/ 173 | 174 | # Ruff stuff: 175 | .ruff_cache/ 176 | 177 | # PyPI configuration file 178 | .pypirc 179 | 180 | # Milvus local databases 181 | *.db 182 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | fail_fast: true 2 | repos: 3 | - repo: https://github.com/astral-sh/ruff-pre-commit 4 | rev: v0.11.6 5 | hooks: 6 | # Run the Ruff formatter. 7 | - id: ruff-format 8 | args: [--config=pyproject.toml] 9 | # Run the Ruff linter. 10 | - id: ruff 11 | args: [--exit-non-zero-on-fix, --fix, --config=pyproject.toml] 12 | files: ^(docling_mcp|tests)/.*\.py$ 13 | - repo: local 14 | hooks: 15 | - id: system 16 | name: MyPy 17 | entry: uv run --no-sync mypy docling_mcp tests 18 | pass_filenames: false 19 | language: system 20 | files: '\.py$' 21 | - repo: local 22 | hooks: 23 | - id: pytest 24 | name: Pytest 25 | language: system 26 | entry: uv run --no-sync pytest --cov=docling_mcp --cov-report=xml tests 27 | pass_filenames: false 28 | files: '\.py$' 29 | - repo: https://github.com/astral-sh/uv-pre-commit 30 | rev: 0.6.16 31 | hooks: 32 | - id: uv-lock 33 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.12 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [v0.3.0](https://github.com/docling-project/docling-mcp/releases/tag/v0.3.0) - 2025-03-20 2 | 3 | ### Feature 4 | 5 | * Initial release with new version number ([`f0671d0`](https://github.com/docling-project/docling-mcp/commit/f0671d070cfb32e2500453b7e693b6cd305829bd)) 6 | 7 | ## [v0.2.0](https://github.com/docling-project/docling-mcp/releases/tag/v0.2.0) - 2025-03-20 8 | 9 | ### Feature 10 | 11 | * Initial release with new version number ([`e6e370e`](https://github.com/docling-project/docling-mcp/commit/e6e370e30c3f5d7f5eda903fe7f56c25d531b13c)) 12 | 13 | ## [v0.1.0](https://github.com/docling-project/docling-mcp/releases/tag/v0.1.0) - 2025-03-20 14 | 15 | ### Feature 16 | 17 | * Initial commit with mcp-server ([#1](https://github.com/docling-project/docling-mcp/issues/1)) ([`a364cc1`](https://github.com/docling-project/docling-mcp/commit/a364cc12c15f36d55785b5498e7fc611991ce394)) 18 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | # This CITATION.cff file was generated with cffinit. 2 | # Visit https://bit.ly/cffinit to generate yours today! 3 | 4 | cff-version: 1.2.0 5 | title: Docling 6 | message: 'If you use Docling, please consider citing as below.' 7 | type: software 8 | authors: 9 | - name: Docling Team 10 | identifiers: 11 | - type: url 12 | value: 'https://arxiv.org/abs/2408.09869' 13 | description: 'arXiv:2408.09869' 14 | repository-code: 'https://github.com/DS4SD/docling' 15 | license: MIT 16 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | This project adheres to the [Docling - Code of Conduct and Covenant](https://github.com/docling-project/community/blob/main/CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing In General 2 | 3 | Our project welcomes external contributions. If you have an itch, please feel 4 | free to scratch it. 5 | 6 | For more details on the contributing guidelines head to the Docling Project [community repository](https://github.com/docling-project/community). 7 | 8 | ## Developing 9 | 10 | ### Clone the project 11 | 12 | Clone this project on your local machine with `git`. For instance, if using an SSH key, run: 13 | 14 | ```bash 15 | git clone git@github.com:docling-project/docling-mcp.git 16 | ``` 17 | 18 | Ensure that your user name and email are properly set: 19 | 20 | ```bash 21 | git config list 22 | ``` 23 | 24 | ### Usage of uv 25 | 26 | We use [uv](https://docs.astral.sh/uv/) as package and project manager. 27 | 28 | #### Installation 29 | 30 | To install `uv`, check the documentation on [Installing uv](https://docs.astral.sh/uv/getting-started/installation/). 31 | 32 | #### Create an environment and sync it 33 | 34 | You can use the `uv sync` to create a project virtual environment (if it does not already exist) and sync 35 | the project's dependencies with the environment. 36 | 37 | ```bash 38 | uv sync 39 | ``` 40 | 41 | #### Use a specific Python version (optional) 42 | 43 | If you need to work with a specific version of Python, you can create a new virtual environment for that version 44 | and run the sync command: 45 | 46 | ```bash 47 | uv venv --python 3.12 48 | uv sync 49 | ``` 50 | 51 | More detailed options are described on the [Using Python environments](https://docs.astral.sh/uv/pip/environments/) documentation. 52 | 53 | #### Add a new dependency 54 | 55 | Simply use the `uv add` command. The `pyproject.toml` and `uv.lock` files will be updated. 56 | 57 | ```bash 58 | uv add [OPTIONS] > 59 | ``` 60 | 61 | ### Code sytle guidelines 62 | 63 | We use the following tools to enforce code style: 64 | 65 | - [Ruff](https://docs.astral.sh/ruff/), as linter and code formatter 66 | - [MyPy](https://mypy.readthedocs.io), as static type checker 67 | 68 | A set of styling checks, as well as regression tests, are defined and managed through the [pre-commit](https://pre-commit.com/) framework. To ensure that those scripts run automatically before a commit is finalized, install `pre-commit` on your local repository: 69 | 70 | ```bash 71 | uv run pre-commit install 72 | ``` 73 | 74 | To run the checks on-demand, type: 75 | 76 | ```bash 77 | uv run pre-commit run --all-files 78 | ``` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Docling Project 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | # MAINTAINERS 2 | 3 | - Peter Staar - [@PeterStaar-IBM](https://github.com/PeterStaar-IBM) 4 | - Christoph Auer - [@cau-git](https://github.com/cau-git) 5 | - Michele Dolfi - [@dolfim-ibm](https://github.com/dolfim-ibm) 6 | - Panos Vagenas - [@vagenas](https://github.com/vagenas) 7 | - Cesar Berrospi Ramis - [@ceberam](https://github.com/ceberam) 8 | 9 | 10 | Maintainers can be contacted at [deepsearch-core@zurich.ibm.com](mailto:deepsearch-core@zurich.ibm.com). 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Docling MCP: making docling agentic 2 | 3 | [![PyPI version](https://img.shields.io/pypi/v/docling-mcp)](https://pypi.org/project/docling-mcp/) 4 | [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/docling-mcp)](https://pypi.org/project/docling-mcp/) 5 | [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv) 6 | [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) 7 | [![Pydantic v2](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/pydantic/pydantic/main/docs/badge/v2.json)](https://pydantic.dev) 8 | [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit) 9 | [![License MIT](https://img.shields.io/github/license/docling-project/docling-mcp)](https://opensource.org/licenses/MIT) 10 | [![PyPI Downloads](https://static.pepy.tech/badge/docling-mcp/month)](https://pepy.tech/projects/docling-mcp) 11 | [![LF AI & Data](https://img.shields.io/badge/LF%20AI%20%26%20Data-003778?logo=linuxfoundation&logoColor=fff&color=0094ff&labelColor=003778)](https://lfaidata.foundation/projects/) 12 | 13 | A document processing service using the Docling-MCP library and MCP (Message Control Protocol) for tool integration. 14 | 15 | > [!NOTE] 16 | > This is an unstable draft implementation which will quickly evolve. 17 | 18 | ## Overview 19 | 20 | Docling MCP is a service that provides tools for document conversion, processing and generation. It uses the Docling library to convert PDF documents into structured formats and provides a caching mechanism to improve performance. The service exposes functionality through a set of tools that can be called by client applications. 21 | 22 | ## Features 23 | 24 | - conversion tools: 25 | - PDF document conversion to structured JSON format (DoclingDocument) 26 | - generation tools: 27 | - Document generation in DoclingDocument, which can be exported to multiple formats 28 | - Local document caching for improved performance 29 | - Support for local files and URLs as document sources 30 | - Memory management for handling large documents 31 | - Logging system for debugging and monitoring 32 | - Milvus upload and retrieval 33 | 34 | ## Getting started 35 | 36 | Install dependencies 37 | 38 | ```sh 39 | uv sync 40 | ``` 41 | 42 | Install the docling_mcp package 43 | 44 | ```sh 45 | uv pip install -e . 46 | ``` 47 | 48 | After installing the dependencies (`uv sync`), you can expose the tools of Docling by running 49 | 50 | ```sh 51 | uv run docling-mcp-server 52 | ``` 53 | 54 | ## Integration with Claude for Desktop 55 | 56 | One of the easiest ways to experiment with the tools provided by Docling-MCP is to leverage [Claude for Desktop](https://claude.ai/download). 57 | Once installed, extend Claude for Desktop so that it can read from your computer’s file system, by following the [For Claude Desktop Users](https://modelcontextprotocol.io/quickstart/user) tutorial. 58 | 59 | To enable Claude for Desktop with Docling MCP, simply edit the config file `claude_desktop_config.json` (located at `~/Library/Application Support/Claude/claude_desktop_config.json` in MacOS) and add a new item in the `mcpServers` key with the details of a Docling MCP server. You can find an example of those details [here](docs/integrations/claude_desktop_config.json). 60 | 61 | 62 | ## Converting documents 63 | 64 | Example of prompt for converting PDF documents: 65 | 66 | ```prompt 67 | Convert the PDF document at into DoclingDocument and return its document-key. 68 | ``` 69 | 70 | ## Generating documents 71 | 72 | Example of prompt for generating new documents: 73 | 74 | ```prompt 75 | I want you to write a Docling document. To do this, you will create a document first by invoking `create_new_docling_document`. Next you can add a title (by invoking `add_title_to_docling_document`) and then iteratively add new section-headings and paragraphs. If you want to insert lists (or nested lists), you will first open a list (by invoking `open_list_in_docling_document`), next add the list_items (by invoking `add_listitem_to_list_in_docling_document`). After adding list-items, you must close the list (by invoking `close_list_in_docling_document`). Nested lists can be created in the same way, by opening and closing additional lists. 76 | 77 | During the writing process, you can check what has been written already by calling the `export_docling_document_to_markdown` tool, which will return the currently written document. At the end of the writing, you must save the document and return me the filepath of the saved document. 78 | 79 | The document should investigate the impact of tokenizers on the quality of LLM's. 80 | ``` 81 | 82 | ## Applications 83 | 84 | ### Milvus RAG configuration 85 | 86 | Copy the .env.example file to .env in the root of the project. 87 | 88 | ```sh 89 | cp .env.example .env 90 | ``` 91 | 92 | If you want to use the RAG Milvus functionality edit the new .env file to set both environment variables. 93 | 94 | ```text 95 | RAG_ENABLED=true 96 | OLLAMA_MODEL=granite3.2:latest 97 | EMBEDDING_MODEL=BAAI/bge-small-en-v1.5 98 | ``` 99 | 100 | Note: 101 | 102 | ollama can be downloaded here https://ollama.com/. Once you have ollama download the model you want to use and then add the model string to the .env file. 103 | 104 | For example we are using `granite3.2:latest` to perform the RAG search. 105 | 106 | To download this model run: 107 | 108 | ```sh 109 | ollama pull granite3.2:latest 110 | ``` 111 | 112 | When using the docling-mcp server with RAG this would be a simple example prompt: 113 | 114 | ```prompt 115 | Process this file /Users/name/example/mock.pdf 116 | 117 | Upload it to the vector store. 118 | 119 | Then summarize xyz that is contained within the document. 120 | ``` 121 | 122 | Known issues 123 | 124 | When restarting the MCP client (e.g. Claude desktop) the client sometimes errors due to the `.milvus_demo.db.lock` file. Delete this before restarting. 125 | 126 | 127 | ## License 128 | 129 | The Docling-MCP codebase is under MIT license. For individual model usage, please refer to the model licenses found in the original packages. 130 | 131 | ## LF AI & Data 132 | 133 | Docling and Docling-MCP is hosted as a project in the [LF AI & Data Foundation](https://lfaidata.foundation/projects/). 134 | 135 | **IBM ❤️ Open Source AI**: The project was started by the AI for knowledge team at IBM Research Zurich. 136 | 137 | [docling_document]: https://docling-project.github.io/docling/concepts/docling_document/ 138 | [integrations]: https://docling-project.github.io/docling-mcp/integrations/ -------------------------------------------------------------------------------- /clients/test_llama_stack.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from llama_stack_client import LlamaStackClient 4 | from llama_stack_client.lib.agents.agent import Agent 5 | from llama_stack_client.lib.agents.event_logger import EventLogger 6 | from llama_stack_client.types.agent_create_params import AgentConfig 7 | from llama_stack_client.types.shared_params.url import URL 8 | from termcolor import cprint 9 | 10 | # Set your model ID 11 | model_id = os.environ["INFERENCE_MODEL"] 12 | client = LlamaStackClient( 13 | base_url=f"http://localhost:{os.environ.get('LLAMA_STACK_PORT', '8080')}" 14 | ) 15 | 16 | # Check available providers 17 | available_providers = client.providers.list() 18 | cprint(f"Available providers: {[p.provider_id for p in available_providers]}", "yellow") 19 | 20 | # Find the MCP provider 21 | mcp_provider = next( 22 | (p for p in available_providers if p.provider_id == "model-context-protocol"), None 23 | ) 24 | if not mcp_provider: 25 | cprint( 26 | "MCP provider not found. Please make sure it's installed and enabled.", "red" 27 | ) 28 | exit(1) 29 | 30 | cprint(f"Using MCP provider: {mcp_provider.provider_id}", "green") 31 | 32 | # Try to unregister the toolgroup if it exists 33 | try: 34 | client.toolgroups.unregister(toolgroup_id="mcp::docling") 35 | cprint("Unregistered existing toolgroup", "yellow") 36 | except Exception: 37 | pass # Ignore if it doesn't exist 38 | 39 | # Register MCP tools 40 | try: 41 | client.toolgroups.register( 42 | toolgroup_id="mcp::docling", 43 | provider_id="model-context-protocol", 44 | mcp_endpoint=URL(uri="http://localhost:8000/sse"), 45 | ) 46 | cprint("Successfully registered MCP docling toolgroup", "green") 47 | except Exception as e: 48 | cprint(f"Error registering MCP toolgroup: {e}", "red") 49 | exit(1) 50 | 51 | # Define an agent with MCP toolgroup 52 | agent_config = AgentConfig( 53 | model=model_id, 54 | instructions="""You are a helpful assistant with access to tools that can convert documents to markdown. 55 | When asked to convert a document, use the 'convert_document' tool. 56 | You can also extract tables with 'extract_tables' or get images with 'convert_document_with_images'. 57 | Always use the appropriate tool when asked to process documents.""", 58 | toolgroups=["mcp::docling"], 59 | input_shields=[], 60 | output_shields=[], 61 | enable_session_persistence=False, 62 | tool_choice="auto", 63 | tool_prompt_format="python_list", 64 | max_tool_calls=3, 65 | ) 66 | 67 | # Create the agent 68 | agent = Agent(client, agent_config) 69 | cprint("Successfully created agent", "green") 70 | 71 | # Create a session 72 | session_id = agent.create_session("test-session") 73 | cprint(f"Created session: {session_id}", "green") 74 | 75 | # Define the prompt 76 | prompt = "Please convert the document at https://arxiv.org/pdf/2004.07606 to markdown and summarize its content." 77 | cprint(f"User> {prompt}", "green") 78 | 79 | try: 80 | # Create a turn with error handling 81 | response = agent.create_turn( 82 | messages=[ 83 | { 84 | "role": "user", 85 | "content": prompt, 86 | } 87 | ], 88 | session_id=session_id, 89 | ) 90 | 91 | # Use try-except for each log to handle potential None values 92 | for log in EventLogger().log(response): 93 | try: 94 | if log is not None: 95 | log.print() 96 | else: 97 | cprint("Received None log entry", "yellow") 98 | except Exception as e: 99 | cprint(f"Error processing log: {e}", "red") 100 | except Exception as e: 101 | cprint(f"Error creating turn: {e}", "red") 102 | import traceback 103 | 104 | traceback.print_exc() 105 | -------------------------------------------------------------------------------- /docling_mcp/__init__.py: -------------------------------------------------------------------------------- 1 | """Main package for Docling MCP server.""" 2 | -------------------------------------------------------------------------------- /docling_mcp/docling_cache.py: -------------------------------------------------------------------------------- 1 | """This module manages the cache directory to run Docling MCP tools.""" 2 | 3 | import hashlib 4 | import json 5 | import os 6 | import sys 7 | from pathlib import Path 8 | from typing import Optional 9 | 10 | from docling_mcp.logger import setup_logger 11 | 12 | # Create a default project logger 13 | logger = setup_logger() 14 | 15 | 16 | def get_cache_dir() -> Path: 17 | """Get the cache directory for the application. 18 | 19 | Returns: 20 | Path: A Path object pointing to the cache directory. 21 | 22 | The function will: 23 | 1. First check for an environment variable 'CACHE_DIR' 24 | 2. If not found, create a '_cache' directory in the root of the current package 25 | 3. Ensure the directory exists before returning 26 | """ 27 | # Check if cache directory is specified in environment variable 28 | cache_dir = os.environ.get("CACHE_DIR") 29 | 30 | if cache_dir: 31 | # Use the directory specified in the environment variable 32 | cache_path = Path(cache_dir) 33 | else: 34 | # Determine the package root directory 35 | if getattr(sys, "frozen", False): 36 | # Handle PyInstaller case 37 | package_root = Path(os.path.dirname(sys.executable)) 38 | else: 39 | # Get the directory of the caller's module 40 | caller_file = sys._getframe(1).f_globals.get("__file__") 41 | 42 | if caller_file: 43 | # If running as a script or module 44 | current_path = Path(caller_file).resolve() 45 | 46 | # Find the package root by looking for the highest directory with an __init__.py 47 | package_root = current_path.parent 48 | while package_root.joinpath("__init__.py").exists(): 49 | package_root = package_root.parent 50 | else: 51 | # Fallback to current working directory if __file__ is not available 52 | package_root = Path.cwd() 53 | 54 | logger.info(f"package-root: {package_root}") 55 | 56 | # Create the cache directory path 57 | cache_path = package_root / "_cache" 58 | 59 | # Ensure cache directory exists 60 | logger.info(f"cache-path: {cache_path}") 61 | os.makedirs(cache_path, exist_ok=True) 62 | 63 | return cache_path 64 | 65 | 66 | def get_cache_key( 67 | source: str, enable_ocr: bool = False, ocr_language: Optional[list[str]] = None 68 | ) -> str: 69 | """Generate a cache key for the document conversion.""" 70 | key_data = { 71 | "source": source, 72 | "enable_ocr": enable_ocr, 73 | "ocr_language": ocr_language or [], 74 | } 75 | key_str = json.dumps(key_data, sort_keys=True) 76 | return hashlib.md5(key_str.encode()).hexdigest() 77 | -------------------------------------------------------------------------------- /docling_mcp/docling_settings.py: -------------------------------------------------------------------------------- 1 | """This module manages the settings for Docling.""" 2 | 3 | from docling.datamodel.pipeline_options import AcceleratorDevice 4 | from docling.datamodel.settings import settings 5 | 6 | from docling_mcp.logger import setup_logger 7 | 8 | # Create a default project logger 9 | logger = setup_logger() 10 | 11 | 12 | # Configure accelerator settings 13 | def configure_accelerator(doc_batch_size: int = 1) -> bool: 14 | """Configure the accelerator device for Docling.""" 15 | try: 16 | # Check if the accelerator_device attribute exists 17 | if hasattr(settings.perf, "accelerator_device"): 18 | # Try to use MPS (Metal Performance Shaders) on macOS 19 | settings.perf.accelerator_device = AcceleratorDevice.MPS 20 | logger.info( 21 | f"Configured accelerator device: {settings.perf.accelerator_device}" 22 | ) 23 | else: 24 | logger.info( 25 | "Accelerator device configuration not supported in this version of Docling" 26 | ) 27 | 28 | # Optimize batch processing 29 | settings.perf.doc_batch_size = doc_batch_size # Process one document at a time 30 | logger.info(f"Configured batch size: {settings.perf.doc_batch_size}") 31 | 32 | return True 33 | except Exception as e: 34 | logger.warning(f"Failed to configure accelerator: {e}") 35 | return False 36 | -------------------------------------------------------------------------------- /docling_mcp/logger.py: -------------------------------------------------------------------------------- 1 | """Utility module for logging.""" 2 | 3 | import logging 4 | 5 | 6 | def setup_logger() -> logging.Logger: 7 | """Setup and return a logger for the entire project.""" 8 | # Create logger 9 | logger = logging.getLogger("docling_mcp") 10 | logger.setLevel(logging.INFO) 11 | 12 | # Create a handler and set its level to INFO 13 | handler = logging.StreamHandler() 14 | handler.setLevel(logging.INFO) 15 | 16 | # Create a formatter and add it to the handler 17 | formatter = logging.Formatter( 18 | "%(asctime)s - %(name)s - %(levelname)s - %(message)s" 19 | ) 20 | handler.setFormatter(formatter) 21 | 22 | # Add the handler to the logger 23 | logger.addHandler(handler) 24 | 25 | return logger 26 | -------------------------------------------------------------------------------- /docling_mcp/servers/mcp_server.py: -------------------------------------------------------------------------------- 1 | """This module initializes and runs the Docling MCP server.""" 2 | 3 | import os 4 | 5 | from docling_mcp.logger import setup_logger 6 | from docling_mcp.shared import mcp 7 | from docling_mcp.tools.conversion import ( 8 | convert_pdf_document_into_json_docling_document_from_uri_path, 9 | is_document_in_local_cache, 10 | ) 11 | from docling_mcp.tools.generation import ( 12 | add_listitem_to_list_in_docling_document, 13 | add_paragraph_to_docling_document, 14 | add_section_heading_to_docling_document, 15 | add_title_to_docling_document, 16 | close_list_in_docling_document, 17 | create_new_docling_document, 18 | export_docling_document_to_markdown, 19 | open_list_in_docling_document, 20 | save_docling_document, 21 | ) 22 | 23 | if ( 24 | os.getenv("RAG_ENABLED") == "true" 25 | and os.getenv("OLLAMA_MODEL") != "" 26 | and os.getenv("EMBEDDING_MODEL") != "" 27 | ): 28 | from docling_mcp.tools.applications import ( 29 | export_docling_document_to_vector_db, 30 | search_documents, 31 | ) 32 | 33 | 34 | def main() -> None: 35 | """Initialize and run the Docling MCP server.""" 36 | # Create a default project logger 37 | logger = setup_logger() 38 | logger.info("starting up Docling MCP-server ...") 39 | 40 | # Initialize and run the server 41 | mcp.run(transport="stdio") 42 | 43 | 44 | if __name__ == "__main__": 45 | main() 46 | -------------------------------------------------------------------------------- /docling_mcp/shared.py: -------------------------------------------------------------------------------- 1 | """This module defines shared resources.""" 2 | 3 | import os 4 | 5 | from dotenv import load_dotenv 6 | from llama_index.core import Settings 7 | from llama_index.core.indices.vector_store.base import VectorStoreIndex 8 | from llama_index.embeddings.huggingface import HuggingFaceEmbedding 9 | from llama_index.llms.ollama import Ollama 10 | from llama_index.node_parser.docling import DoclingNodeParser 11 | from llama_index.vector_stores.milvus import MilvusVectorStore 12 | from mcp.server.fastmcp import FastMCP 13 | 14 | from docling_core.types.doc.document import ( 15 | DoclingDocument, 16 | NodeItem, 17 | ) 18 | 19 | load_dotenv() 20 | 21 | 22 | # Create a single shared FastMCP instance 23 | mcp = FastMCP("docling") 24 | 25 | # Define your shared cache here if it's used by multiple tools 26 | local_document_cache: dict[str, DoclingDocument] = {} 27 | local_stack_cache: dict[str, list[NodeItem]] = {} 28 | 29 | OLLAMA_MODEL: str | None = os.getenv("OLLAMA_MODEL") 30 | EMBEDDING_MODEL: str | None = os.getenv("EMBEDDING_MODEL") 31 | 32 | 33 | if ( 34 | os.getenv("RAG_ENABLED") == "true" 35 | and OLLAMA_MODEL is not None 36 | and EMBEDDING_MODEL is not None 37 | ): 38 | embed_model = HuggingFaceEmbedding(model_name=EMBEDDING_MODEL) 39 | Settings.embed_model = embed_model 40 | Settings.llm = Ollama(model=OLLAMA_MODEL, request_timeout=120.0) 41 | 42 | node_parser = DoclingNodeParser() 43 | 44 | embed_dim = len(embed_model.get_text_embedding("hi")) 45 | 46 | milvus_vector_store = MilvusVectorStore( 47 | uri="./milvus_demo.db", dim=embed_dim, overwrite=True 48 | ) 49 | 50 | local_index_cache: dict[str, VectorStoreIndex] = {} 51 | -------------------------------------------------------------------------------- /docling_mcp/tools/__init__.py: -------------------------------------------------------------------------------- 1 | """MCP tools for Docling. 2 | 3 | Currently supporting: 4 | - converting documents into DoclingDocument objects 5 | - generating Docling documents incrementally 6 | """ 7 | -------------------------------------------------------------------------------- /docling_mcp/tools/applications.py: -------------------------------------------------------------------------------- 1 | """This module defines applications.""" 2 | 3 | import os 4 | from typing import Any 5 | 6 | from docling_core.types.doc.document import DoclingDocument 7 | 8 | from docling_mcp.logger import setup_logger 9 | from docling_mcp.shared import local_document_cache, mcp 10 | 11 | logger = setup_logger() 12 | 13 | if ( 14 | os.getenv("RAG_ENABLED") == "true" 15 | and os.getenv("OLLAMA_MODEL") != "" 16 | and os.getenv("EMBEDDING_MODEL") != "" 17 | ): 18 | import json 19 | 20 | from llama_index.core import Document, StorageContext, VectorStoreIndex 21 | from llama_index.core.base.response.schema import ( 22 | RESPONSE_TYPE, 23 | Response, 24 | ) 25 | from mcp.shared.exceptions import McpError 26 | from mcp.types import INTERNAL_ERROR, ErrorData 27 | 28 | from docling_mcp.shared import local_index_cache, milvus_vector_store, node_parser 29 | 30 | @mcp.tool() 31 | def export_docling_document_to_vector_db(document_key: str) -> str: 32 | """Exports a document from the local document cache to a vector database for search capabilities. 33 | 34 | This tool converts a Docling document that exists in the local cache into markdown format, 35 | then loads it into a vector database index. This allows the document to be searched using 36 | semantic search techniques. 37 | 38 | Args: 39 | document_key (str): The unique identifier for the document in the local cache. 40 | 41 | Returns: 42 | str: A confirmation message indicating the document was successfully indexed. 43 | 44 | Raises: 45 | ValueError: If the specified document_key does not exist in the local cache. 46 | 47 | Example: 48 | export_docling_document_to_vector_db("doc123") 49 | """ 50 | if document_key not in local_document_cache: 51 | doc_keys = ", ".join(local_document_cache.keys()) 52 | raise ValueError( 53 | f"document-key: {document_key} is not found. Existing document-keys are: {doc_keys}" 54 | ) 55 | 56 | docling_document: DoclingDocument = local_document_cache[document_key] 57 | document_dict: dict[str, Any] = docling_document.export_to_dict() 58 | document_json: str = json.dumps(document_dict) 59 | 60 | document = Document( 61 | text=document_json, 62 | metadata={"filename": docling_document.name}, 63 | ) 64 | 65 | index = VectorStoreIndex.from_documents( 66 | documents=[document], 67 | transformations=[node_parser], 68 | storage_context=StorageContext.from_defaults( 69 | vector_store=milvus_vector_store 70 | ), 71 | ) 72 | 73 | index.insert(document) 74 | 75 | local_index_cache["milvus_index"] = index 76 | 77 | return f"Successful initialisation for document with id {document_key}" 78 | 79 | @mcp.tool() 80 | def search_documents(query: str) -> str: 81 | """Searches through previously uploaded and indexed documents using semantic search. 82 | 83 | This function retrieves relevant information from documents that have been processed 84 | and added to the vector database. It uses semantic similarity to find content that 85 | best matches the query, rather than simple keyword matching. 86 | 87 | Args: 88 | query (str): The search query text used to find relevant information in the indexed documents. 89 | 90 | Returns: 91 | str: A string containing the relevant contextual information retrieved from the documents 92 | that best matches the query. 93 | 94 | Example: 95 | search_documents("What are the main findings about climate change?") 96 | """ 97 | index = local_index_cache["milvus_index"] 98 | 99 | query_engine = index.as_query_engine() 100 | response: RESPONSE_TYPE = query_engine.query(query) 101 | 102 | if isinstance(response, Response): 103 | if response.response is not None: 104 | return response.response 105 | else: 106 | raise McpError( 107 | ErrorData( 108 | code=INTERNAL_ERROR, 109 | message="Response object has no response content", 110 | ) 111 | ) 112 | else: 113 | raise McpError( 114 | ErrorData( 115 | code=INTERNAL_ERROR, 116 | message=f"Unexpected response type: {type(response)}", 117 | ) 118 | ) 119 | -------------------------------------------------------------------------------- /docling_mcp/tools/conversion.py: -------------------------------------------------------------------------------- 1 | """Tools for converting documents into DoclingDocument objects.""" 2 | 3 | import gc 4 | from typing import Annotated, Any 5 | 6 | from mcp.shared.exceptions import McpError 7 | from mcp.types import INTERNAL_ERROR, ErrorData 8 | 9 | from docling.datamodel.base_models import InputFormat 10 | from docling.datamodel.pipeline_options import ( 11 | AcceleratorDevice, 12 | PdfPipelineOptions, 13 | ) 14 | from docling.document_converter import DocumentConverter, FormatOption, PdfFormatOption 15 | from docling_core.types.doc.document import ( 16 | ContentLayer, 17 | ) 18 | from docling_core.types.doc.labels import ( 19 | DocItemLabel, 20 | ) 21 | 22 | from docling_mcp.docling_cache import get_cache_key 23 | from docling_mcp.logger import setup_logger 24 | from docling_mcp.shared import local_document_cache, local_stack_cache, mcp 25 | 26 | # Create a default project logger 27 | logger = setup_logger() 28 | 29 | 30 | def cleanup_memory() -> None: 31 | """Force garbage collection to free up memory.""" 32 | logger.info("Performed memory cleanup") 33 | gc.collect() 34 | 35 | 36 | @mcp.tool() 37 | def is_document_in_local_cache(cache_key: str) -> bool: 38 | """Verify if document is already converted and in the local cache. 39 | 40 | Args: 41 | cache_key: Document identifier in the cache. 42 | 43 | Returns: 44 | Whether the document is already converted and in the local cache. 45 | """ 46 | return cache_key in local_document_cache 47 | 48 | 49 | @mcp.tool() 50 | def convert_pdf_document_into_json_docling_document_from_uri_path( 51 | source: str, 52 | ) -> tuple[bool, str]: 53 | """Convert a PDF document from a URL or local path and store in local cache. 54 | 55 | Args: 56 | source: URL or local file path to the document 57 | 58 | Returns: 59 | The tools returns a tuple, the first element being a boolean 60 | representing success and the second for the cache_key to allow 61 | future access to the file. 62 | 63 | Usage: 64 | convert_document("https://arxiv.org/pdf/2408.09869") 65 | convert_document("/path/to/document.pdf") 66 | """ 67 | try: 68 | # Remove any quotes from the source string 69 | source = source.strip("\"'") 70 | 71 | # Log the cleaned source 72 | logger.info(f"Processing document from source: {source}") 73 | 74 | # Generate cache key 75 | cache_key = get_cache_key(source) 76 | 77 | if cache_key in local_document_cache: 78 | logger.info(f"{source} has previously been added.") 79 | return False, "Document already exists in the system cache." 80 | 81 | # Log the start of processing 82 | logger.info("Set up pipeline options") 83 | 84 | # Configure pipeline 85 | # ocr_options = EasyOcrOptions(lang=ocr_language or ["en"]) 86 | pipeline_options = PdfPipelineOptions( 87 | # do_ocr=False, 88 | # ocr_options=ocr_options, 89 | accelerator_device=AcceleratorDevice.MPS # Explicitly set MPS 90 | ) 91 | format_options: dict[InputFormat, FormatOption] = { 92 | InputFormat.PDF: PdfFormatOption(pipeline_options=pipeline_options) 93 | } 94 | 95 | # Create converter with MPS acceleration 96 | logger.info(f"Creating DocumentConverter with format_options: {format_options}") 97 | converter = DocumentConverter(format_options=format_options) 98 | 99 | # Convert the document 100 | logger.info("Start conversion") 101 | result = converter.convert(source) 102 | 103 | # Check for errors - handle different API versions 104 | has_error = False 105 | error_message = "" 106 | 107 | # Try different ways to check for errors based on the API version 108 | if hasattr(result, "status"): 109 | if hasattr(result.status, "is_error"): 110 | has_error = result.status.is_error 111 | elif hasattr(result.status, "error"): 112 | has_error = result.status.error 113 | 114 | if hasattr(result, "errors") and result.errors: 115 | has_error = True 116 | error_message = str(result.errors) 117 | 118 | if has_error: 119 | error_msg = f"Conversion failed: {error_message}" 120 | raise McpError(ErrorData(code=INTERNAL_ERROR, message=error_msg)) 121 | 122 | local_document_cache[cache_key] = result.document 123 | 124 | item = result.document.add_text( 125 | label=DocItemLabel.TEXT, 126 | text=f"source: {source}", 127 | content_layer=ContentLayer.FURNITURE, 128 | ) 129 | 130 | local_stack_cache[cache_key] = [item] 131 | 132 | # Log completion 133 | logger.info(f"Successfully created the Docling document: {source}") 134 | 135 | # Clean up memory 136 | cleanup_memory() 137 | 138 | return True, cache_key 139 | 140 | except Exception as e: 141 | logger.exception(f"Error converting document: {source}") 142 | raise McpError( 143 | ErrorData(code=INTERNAL_ERROR, message=f"Unexpected error: {e!s}") 144 | ) from e 145 | 146 | 147 | @mcp.tool() 148 | def convert_attachments_into_docling_document( 149 | pdf_payloads: list[Annotated[bytes, {"media_type": "application/octet-stream"}]], 150 | ) -> list[dict[str, Any]]: 151 | """Process a pdf files attachment from Claude Desktop. 152 | 153 | Args: 154 | pdf_payloads: PDF document as binary data from the attachment 155 | 156 | Returns: 157 | A dictionary with processed results 158 | """ 159 | results = [] 160 | for pdf_payload in pdf_payloads: 161 | # Example processing - you can replace this with your actual processing logic 162 | file_size = len(pdf_payload) 163 | 164 | # First few bytes as hex for identification 165 | header_bytes = pdf_payload[:10].hex() 166 | 167 | # You can implement file type detection, parsing, or any other processing here 168 | # For example, if it's an image, you might use PIL to process it 169 | 170 | results.append( 171 | { 172 | "file_size_bytes": file_size, 173 | "header_hex": header_bytes, 174 | "status": "processed", 175 | } 176 | ) 177 | 178 | return results 179 | -------------------------------------------------------------------------------- /docling_mcp/tools/generation.py: -------------------------------------------------------------------------------- 1 | """Tools for generating Docling documents.""" 2 | 3 | import hashlib 4 | from io import BytesIO 5 | 6 | # from bs4 import BeautifulSoup # , NavigableString, PageElement, Tag 7 | from docling.datamodel.base_models import ConversionStatus, InputFormat 8 | from docling.datamodel.document import ( 9 | ConversionResult, 10 | ) 11 | from docling.document_converter import DocumentConverter 12 | 13 | # from docling.backend.html_backend import HTMLDocumentBackend 14 | from docling_core.types.doc.document import ( 15 | ContentLayer, 16 | DoclingDocument, 17 | GroupItem, 18 | ) 19 | from docling_core.types.doc.labels import ( 20 | DocItemLabel, 21 | GroupLabel, 22 | # PictureClassificationLabel, 23 | # TableCellLabel 24 | ) 25 | from docling_core.types.io import DocumentStream 26 | 27 | from docling_mcp.docling_cache import get_cache_dir 28 | from docling_mcp.logger import setup_logger 29 | from docling_mcp.shared import local_document_cache, local_stack_cache, mcp 30 | 31 | # Create a default project logger 32 | logger = setup_logger() 33 | 34 | 35 | def hash_string_md5(input_string: str) -> str: 36 | """Creates an md5 hash-string from the input string.""" 37 | return hashlib.md5(input_string.encode()).hexdigest() 38 | 39 | 40 | @mcp.tool() 41 | def create_new_docling_document(prompt: str) -> str: 42 | """Creates a new Docling document from a provided prompt string. 43 | 44 | This function generates a new document in the local document cache with the 45 | provided prompt text. The document is assigned a unique key derived from an MD5 46 | hash of the prompt text. 47 | 48 | Args: 49 | prompt (str): The prompt text to include in the new document. 50 | 51 | Returns: 52 | str: A confirmation message containing the document key and original prompt. 53 | 54 | Note: 55 | The document is stored in the local_document_cache with a key generated 56 | from the MD5 hash of the prompt string, ensuring uniqueness and retrievability. 57 | 58 | Example: 59 | create_new_docling_document("Analyze the impact of climate change on marine ecosystems") 60 | """ 61 | doc = DoclingDocument(name="Generated Document") 62 | 63 | item = doc.add_text( 64 | label=DocItemLabel.TEXT, 65 | text=f"prompt: {prompt}", 66 | content_layer=ContentLayer.FURNITURE, 67 | ) 68 | 69 | document_key = hash_string_md5(prompt) 70 | 71 | local_document_cache[document_key] = doc 72 | local_stack_cache[document_key] = [item] 73 | 74 | return f"document-key: {document_key} for prompt:`{prompt}`" 75 | 76 | 77 | @mcp.tool() 78 | def export_docling_document_to_markdown(document_key: str) -> str: 79 | """Exports a document from the local document cache to markdown format. 80 | 81 | This tool converts a Docling document that exists in the local cache into 82 | a markdown formatted string, which can be used for display or further processing. 83 | 84 | Args: 85 | document_key (str): The unique identifier for the document in the local cache. 86 | 87 | Returns: 88 | str: A string containing the markdown representation of the document, 89 | along with a header identifying the document key. 90 | 91 | Raises: 92 | ValueError: If the specified document_key does not exist in the local cache. 93 | 94 | Example: 95 | export_docling_document_to_markdown("doc123") 96 | """ 97 | if document_key not in local_document_cache: 98 | doc_keys = ", ".join(local_document_cache.keys()) 99 | raise ValueError( 100 | f"document-key: {document_key} is not found. Existing document-keys are: {doc_keys}" 101 | ) 102 | 103 | markdown = local_document_cache[document_key].export_to_markdown() 104 | 105 | return f"Markdown export for document with key: {document_key}\n\n{markdown}\n\n" 106 | 107 | 108 | @mcp.tool() 109 | def save_docling_document(document_key: str) -> str: 110 | """Saves a document from the local document cache to disk in both markdown and JSON formats. 111 | 112 | This tool takes a document that exists in the local cache and saves it to the specified 113 | cache directory with filenames based on the document key. Both markdown and JSON versions 114 | of the document are saved. 115 | 116 | Args: 117 | document_key (str): The unique identifier for the document in the local cache. 118 | 119 | Returns: 120 | str: A confirmation message indicating where the document was saved. 121 | 122 | Raises: 123 | ValueError: If the specified document_key does not exist in the local cache. 124 | 125 | Example: 126 | save_docling_document("doc123") 127 | """ 128 | if document_key not in local_document_cache: 129 | doc_keys = ", ".join(local_document_cache.keys()) 130 | raise ValueError( 131 | f"document-key: {document_key} is not found. Existing document-keys are: {doc_keys}" 132 | ) 133 | 134 | cache_dir = get_cache_dir() 135 | 136 | local_document_cache[document_key].save_as_markdown( 137 | filename=cache_dir / f"{document_key}.md", text_width=72 138 | ) 139 | local_document_cache[document_key].save_as_json( 140 | filename=cache_dir / f"{document_key}.json" 141 | ) 142 | 143 | filename = cache_dir / f"{document_key}.md" 144 | 145 | return f"document saved at {filename}" 146 | 147 | 148 | @mcp.tool() 149 | def add_title_to_docling_document(document_key: str, title: str) -> str: 150 | """Adds or updates the title of a document in the local document cache. 151 | 152 | This tool modifies an existing document that has already been processed 153 | and stored in the local cache. It requires that the document already exists 154 | in the cache before a title can be added. 155 | 156 | Args: 157 | document_key (str): The unique identifier for the document in the local cache. 158 | title (str): The title text to add to the document. 159 | 160 | Returns: 161 | str: A confirmation message indicating the title was updated successfully. 162 | 163 | Raises: 164 | ValueError: If the specified document_key does not exist in the local cache. 165 | 166 | Example: 167 | add_title_to_docling_document("doc123", "Research Paper on Climate Change") 168 | """ 169 | if document_key not in local_document_cache: 170 | doc_keys = ", ".join(local_document_cache.keys()) 171 | raise ValueError( 172 | f"document-key: {document_key} is not found. Existing document-keys are: {doc_keys}" 173 | ) 174 | 175 | if len(local_stack_cache[document_key]) == 0: 176 | raise ValueError( 177 | f"Stack size is zero for document with document-key: {document_key}. Abort document generation" 178 | ) 179 | 180 | parent = local_stack_cache[document_key][-1] 181 | 182 | if isinstance(parent, GroupItem): 183 | if parent.label == GroupLabel.LIST or parent.label == GroupLabel.ORDERED_LIST: 184 | raise ValueError( 185 | "A list is currently opened. Please close the list before adding a title!" 186 | ) 187 | 188 | item = local_document_cache[document_key].add_title(text=title) 189 | local_stack_cache[document_key][-1] = item 190 | 191 | return f"updated title for document with key: {document_key}" 192 | 193 | 194 | @mcp.tool() 195 | def add_section_heading_to_docling_document( 196 | document_key: str, section_heading: str, section_level: int 197 | ) -> str: 198 | """Adds a section heading to an existing document in the local document cache. 199 | 200 | This tool inserts a section heading with the specified heading text and level 201 | into a document that has already been processed and stored in the local cache. 202 | Section levels typically represent heading hierarchy (e.g., 1 for H1, 2 for H2). 203 | 204 | Args: 205 | document_key (str): The unique identifier for the document in the local cache. 206 | section_heading (str): The text to use for the section heading. 207 | section_level (int): The level of the heading (1-6, where 1 is the highest level). 208 | 209 | Returns: 210 | str: A confirmation message indicating the heading was added successfully. 211 | 212 | Raises: 213 | ValueError: If the specified document_key does not exist in the local cache. 214 | 215 | Example: 216 | add_section_heading_to_docling_document("doc123", "Introduction", 1) 217 | """ 218 | if document_key not in local_document_cache: 219 | doc_keys = ", ".join(local_document_cache.keys()) 220 | raise ValueError( 221 | f"document-key: {document_key} is not found. Existing document-keys are: {doc_keys}" 222 | ) 223 | 224 | if len(local_stack_cache[document_key]) == 0: 225 | raise ValueError( 226 | f"Stack size is zero for document with document-key: {document_key}. Abort document generation" 227 | ) 228 | 229 | parent = local_stack_cache[document_key][-1] 230 | 231 | if isinstance(parent, GroupItem): 232 | if parent.label == GroupLabel.LIST or parent.label == GroupLabel.ORDERED_LIST: 233 | raise ValueError( 234 | "A list is currently opened. Please close the list before adding a section-heading!" 235 | ) 236 | 237 | item = local_document_cache[document_key].add_heading( 238 | text=section_heading, level=section_level 239 | ) 240 | local_stack_cache[document_key][-1] = item 241 | 242 | return f"added section-heading of level {section_level} for document with key: {document_key}" 243 | 244 | 245 | @mcp.tool() 246 | def add_paragraph_to_docling_document(document_key: str, paragraph: str) -> str: 247 | """Adds a paragraph of text to an existing document in the local document cache. 248 | 249 | This tool inserts a new paragraph under the specified section header and level 250 | into a document that has already been processed and stored in the cache. 251 | 252 | Args: 253 | document_key (str): The unique identifier for the document in the local cache. 254 | paragraph (str): The text content to add as a paragraph. 255 | 256 | Returns: 257 | str: A confirmation message indicating the paragraph was added successfully. 258 | 259 | Raises: 260 | ValueError: If the specified document_key does not exist in the local cache. 261 | 262 | Example: 263 | add_paragraph_to_docling_document("doc123", "This is a sample paragraph text.", 2) 264 | """ 265 | if document_key not in local_document_cache: 266 | doc_keys = ", ".join(local_document_cache.keys()) 267 | raise ValueError( 268 | f"document-key: {document_key} is not found. Existing document-keys are: {doc_keys}" 269 | ) 270 | 271 | if len(local_stack_cache[document_key]) == 0: 272 | raise ValueError( 273 | f"Stack size is zero for document with document-key: {document_key}. Abort document generation" 274 | ) 275 | 276 | parent = local_stack_cache[document_key][-1] 277 | 278 | if isinstance(parent, GroupItem): 279 | if parent.label == GroupLabel.LIST or parent.label == GroupLabel.ORDERED_LIST: 280 | raise ValueError( 281 | "A list is currently opened. Please close the list before adding a paragraph!" 282 | ) 283 | 284 | item = local_document_cache[document_key].add_text( 285 | label=DocItemLabel.TEXT, text=paragraph 286 | ) 287 | local_stack_cache[document_key][-1] = item 288 | 289 | return f"added paragraph for document with key: {document_key}" 290 | 291 | 292 | @mcp.tool() 293 | def open_list_in_docling_document(document_key: str) -> str: 294 | """Opens a new list group in an existing document in the local document cache. 295 | 296 | This tool creates a new list structure within a document that has already been 297 | processed and stored in the local cache. It requires that the document already exists 298 | and that there is at least one item in the document's stack cache. 299 | 300 | Args: 301 | document_key (str): The unique identifier for the document in the local cache. 302 | 303 | Returns: 304 | str: A confirmation message indicating the list was successfully opened. 305 | 306 | Raises: 307 | ValueError: If the specified document_key does not exist in the local cache. 308 | 309 | Example: 310 | open_list_docling_document(document_key="doc123") 311 | """ 312 | if document_key not in local_document_cache: 313 | doc_keys = ", ".join(local_document_cache.keys()) 314 | raise ValueError( 315 | f"document-key: {document_key} is not found. Existing document-keys are: {doc_keys}" 316 | ) 317 | 318 | if len(local_stack_cache[document_key]) == 0: 319 | raise ValueError( 320 | f"Stack size is zero for document with document-key: {document_key}. Abort document generation" 321 | ) 322 | 323 | item = local_document_cache[document_key].add_group(label=GroupLabel.LIST) 324 | local_stack_cache[document_key].append(item) 325 | 326 | return f"opened a new list for document with key: {document_key}" 327 | 328 | 329 | @mcp.tool() 330 | def close_list_in_docling_document(document_key: str) -> str: 331 | """Closes a list group in an existing document in the local document cache. 332 | 333 | This tool closes a previously opened list structure within a document. 334 | It requires that the document exists and that there is more than one item 335 | in the document's stack cache. 336 | 337 | Args: 338 | document_key (str): The unique identifier for the document in the local cache. 339 | 340 | Returns: 341 | str: A confirmation message indicating the list was successfully closed. 342 | 343 | Raises: 344 | ValueError: If the specified document_key does not exist in the local cache. 345 | 346 | Example: 347 | close_list_docling_document(document_key="doc123") 348 | """ 349 | if document_key not in local_document_cache: 350 | doc_keys = ", ".join(local_document_cache.keys()) 351 | raise ValueError( 352 | f"document-key: {document_key} is not found. Existing document-keys are: {doc_keys}" 353 | ) 354 | 355 | if len(local_stack_cache[document_key]) <= 1: 356 | raise ValueError( 357 | f"Stack size is zero for document with document-key: {document_key}. Abort document generation" 358 | ) 359 | 360 | local_stack_cache[document_key].pop() 361 | 362 | return f"closed list for document with key: {document_key}" 363 | 364 | 365 | @mcp.tool() 366 | def add_listitem_to_list_in_docling_document( 367 | document_key: str, listitem_text: str, listmarker_text: str 368 | ) -> str: 369 | """Adds a list item to an open list in an existing document in the local document cache. 370 | 371 | This tool inserts a new list item with the specified text and marker into an 372 | open list within a document. It requires that the document exists and that 373 | there is at least one item in the document's stack cache. 374 | 375 | Args: 376 | document_key (str): The unique identifier for the document in the local cache. 377 | listitem_text (str): The content text for the list item. 378 | listmarker_text (str): The marker text to use for the list item (e.g., "-", "1.", "•"). 379 | 380 | Returns: 381 | str: A confirmation message indicating the list item was successfully added. 382 | 383 | Raises: 384 | ValueError: If the specified document_key does not exist in the local cache. 385 | 386 | Example: 387 | add_listitem_to_docling_document(document_key="doc123", listitem_text="First item in the list", listmarker_text="-") 388 | """ 389 | if document_key not in local_document_cache: 390 | doc_keys = ", ".join(local_document_cache.keys()) 391 | raise ValueError( 392 | f"document-key: {document_key} is not found. Existing document-keys are: {doc_keys}" 393 | ) 394 | 395 | if len(local_stack_cache[document_key]) == 0: 396 | raise ValueError( 397 | f"Stack size is zero for document with document-key: {document_key}. Abort document generation" 398 | ) 399 | 400 | parent = local_stack_cache[document_key][-1] 401 | 402 | if isinstance(parent, GroupItem): 403 | if parent.label != GroupLabel.LIST and parent.label != GroupLabel.ORDERED_LIST: 404 | raise ValueError( 405 | "No list is currently opened. Please open a list before adding list-items!" 406 | ) 407 | else: 408 | raise ValueError( 409 | "No list is currently opened. Please open a list before adding list-items!" 410 | ) 411 | 412 | local_document_cache[document_key].add_list_item( 413 | text=listitem_text, marker=listmarker_text, parent=parent 414 | ) 415 | 416 | return f"added listitem to list in document with key: {document_key}" 417 | 418 | 419 | @mcp.tool() 420 | def add_table_in_html_format_to_docling_document( 421 | document_key: str, 422 | html_table: str, 423 | table_captions: list[str] | None = None, 424 | table_footnotes: list[str] | None = None, 425 | ) -> str: 426 | """Adds an HTML-formatted table to an existing document in the local document cache. 427 | 428 | This tool parses the provided HTML table string, converts it to a structured table 429 | representation, and adds it to the specified document. It also supports optional 430 | captions and footnotes for the table. 431 | 432 | Args: 433 | document_key (str): The unique identifier for the document in the local cache. 434 | html_table (str): The HTML string representation of the table to add. 435 | table_captions (list[str], optional): A list of caption strings to associate with the table. 436 | table_footnotes (list[str], optional): A list of footnote strings to associate with the table. 437 | 438 | Returns: 439 | str: A confirmation message indicating the table was successfully added. 440 | 441 | Raises: 442 | ValueError: If the specified document_key does not exist in the local cache. 443 | ValueError: If the stack size for the document is zero. 444 | HTMLParseError: If the provided HTML table string cannot be properly parsed. 445 | 446 | Example: 447 | add_table_in_html_format_to_docling_document( 448 | document_key="doc123", 449 | html_table="
NameAge
John30
", 450 | table_captions=["Table 1: Sample demographic data"], 451 | table_footnotes=["Data collected in 2023"] 452 | ) 453 | 454 | Example with rowspan and colspan: 455 | add_table_in_html_format_to_docling_document( 456 | document_key="doc123", 457 | html_table="
Demographics
NameAge
John30
Jane
", 458 | table_captions=["Table 2: Complex demographic data with merged cells"] 459 | ) 460 | """ 461 | if document_key not in local_document_cache: 462 | doc_keys = ", ".join(local_document_cache.keys()) 463 | raise ValueError( 464 | f"document-key: {document_key} is not found. Existing document-keys are: {doc_keys}" 465 | ) 466 | 467 | doc = local_document_cache[document_key] 468 | 469 | if len(local_stack_cache[document_key]) == 0: 470 | raise ValueError( 471 | f"Stack size is zero for document with document-key: {document_key}. Abort document generation" 472 | ) 473 | 474 | html_doc: str = f"{html_table}" 475 | 476 | buff = BytesIO(html_doc.encode("utf-8")) 477 | doc_stream = DocumentStream(name="tmp", stream=buff) 478 | 479 | converter = DocumentConverter(allowed_formats=[InputFormat.HTML]) 480 | conv_result: ConversionResult = converter.convert(doc_stream) 481 | 482 | if ( 483 | conv_result.status == ConversionStatus.SUCCESS 484 | and len(conv_result.document.tables) > 0 485 | ): 486 | table = doc.add_table(data=conv_result.document.tables[0].data) 487 | 488 | for _ in table_captions or []: 489 | caption = doc.add_text(label=DocItemLabel.CAPTION, text=_) 490 | table.captions.append(caption.get_ref()) 491 | 492 | for _ in table_footnotes or []: 493 | footnote = doc.add_text(label=DocItemLabel.FOOTNOTE, text=_) 494 | table.footnotes.append(footnote.get_ref()) 495 | else: 496 | raise ValueError( 497 | "Could not parse the html string of the table! Please fix the html and try again!" 498 | ) 499 | 500 | return f"Added table to a document with key: {document_key}" 501 | -------------------------------------------------------------------------------- /docs/integrations/claude_desktop_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "docling": { 4 | "command": "", 5 | "args": [ 6 | "--directory", 7 | "", 8 | "run", 9 | "docling-mcp-server" 10 | ] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "docling-mcp" 7 | version = "0.3.0" # DO NOT EDIT, updated automatically 8 | description = "Running Docling as an agent using tools" 9 | license = "MIT" 10 | license-files = ["LICENSE"] 11 | authors = [ 12 | {name="Peter Staar", email="taa@zurich.ibm.com"}, 13 | {name="Adel Zaalouk", email="azaalouk@redhat.com"}, 14 | {name="Michele Dolfi", email="dol@zurich.ibm.com"}, 15 | {name="Panos Vagenas", email="pva@zurich.ibm.com"}, 16 | {name="Christoph Auer", email="cau@zurich.ibm.com"}, 17 | {name="Cesar Berrospi Ramis", email="ceb@zurich.ibm.com"}, 18 | ] 19 | maintainers = [ 20 | {name="Peter Staar", email="taa@zurich.ibm.com"}, 21 | {name="Adel Zaalouk", email="azaalouk@redhat.com"}, 22 | {name="Michele Dolfi", email="dol@zurich.ibm.com"}, 23 | {name="Panos Vagenas", email="pva@zurich.ibm.com"}, 24 | {name="Christoph Auer", email="cau@zurich.ibm.com"}, 25 | {name="Cesar Berrospi Ramis", email="ceb@zurich.ibm.com"}, 26 | ] 27 | readme = "README.md" 28 | keywords = ["mcp", "message control protocol", "agents", "agentic", "AI", "artificial intelligence", "document understanding", "RAG", "Docling"] 29 | classifiers = [ 30 | "Intended Audience :: Developers", 31 | "Intended Audience :: Science/Research", 32 | "License :: OSI Approved :: MIT License", 33 | "Operating System :: OS Independent", 34 | "Programming Language :: Python :: 3", 35 | "Typing :: Typed", 36 | "Topic :: Scientific/Engineering :: Artificial Intelligence", 37 | "Topic :: Software Development :: Build Tools", 38 | ] 39 | requires-python = ">=3.10" 40 | dependencies = [ 41 | "docling~=2.25", 42 | "httpx>=0.28.1", 43 | "llama-index>=0.12.33", 44 | "llama-index-core>=0.12.28", 45 | "llama-index-embeddings-huggingface>=0.5.2", 46 | "llama-index-embeddings-openai>=0.3.1", 47 | "llama-index-llms-ollama>=0.5.4", 48 | "llama-index-node-parser-docling>=0.3.1", 49 | "llama-index-readers-docling>=0.3.2", 50 | "llama-index-readers-file>=0.4.7", 51 | "llama-index-vector-stores-milvus>=0.7.2", 52 | "mcp[cli]>=1.4.0", 53 | "pydantic~=2.10", 54 | "pydantic-settings~=2.4", 55 | "python-dotenv>=1.1.0", 56 | ] 57 | 58 | [project.optional-dependencies] 59 | tesserocr = [ 60 | "tesserocr~=2.7" 61 | ] 62 | rapidocr = [ 63 | "rapidocr-onnxruntime~=1.4; python_version<'3.13'", 64 | "onnxruntime~=1.7", 65 | ] 66 | 67 | [dependency-groups] 68 | dev = [ 69 | "mypy~=1.11", 70 | "pre-commit-uv~=4.1", 71 | "pytest~=8.3", 72 | "pytest-check~=2.4", 73 | "pytest-cov>=6.1.1", 74 | "python-semantic-release~=7.32", 75 | "ruff>=0.9.6", 76 | ] 77 | 78 | [project.scripts] 79 | docling-mcp-server = "docling_mcp.servers.mcp_server:main" 80 | 81 | [project.urls] 82 | Homepage = "https://github.com/docling-project/docling-mcp" 83 | Repository = "https://github.com/docling-project/docling-mcp" 84 | Issues = "https://github.com/docling-project/docling-mcp/issues" 85 | Changelog = "https://github.com/docling-project/docling-mcp/blob/main/CHANGELOG.md" 86 | 87 | [tool.hatch.build.targets.sdist] 88 | include = ["docling_mcp"] 89 | 90 | [tool.ruff] 91 | line-length = 88 92 | indent-width = 4 93 | respect-gitignore = true 94 | include = ["docling_mcp", "tests"] 95 | 96 | [tool.ruff.format] 97 | skip-magic-trailing-comma = false 98 | docstring-code-format = true 99 | docstring-code-line-length = "dynamic" 100 | 101 | [tool.ruff.lint] 102 | select = [ 103 | "B", # flake8-bugbear 104 | "C", # flake8-comprehensions, mccabe 105 | "D", # flake8-docstrings 106 | "E", # pycodestyle errors (default) 107 | "F", # pyflakes (default) 108 | "I", # isort 109 | "PD", # pandas-vet 110 | "PIE", # pie 111 | # "PTH", # pathlib 112 | "Q", # flake8-quotes 113 | # "RET", # return 114 | "RUF", # Enable all ruff-specific checks 115 | # "SIM", # simplify 116 | "S307", # eval 117 | # "T20", # (disallow print statements) keep debugging statements out of the codebase 118 | "W", # pycodestyle warnings 119 | "ASYNC", # async 120 | "UP", # pyupgrade 121 | ] 122 | ignore = [ 123 | "E501", # Line too long, handled by ruff formatter 124 | "D107", # "Missing docstring in __init__", 125 | "F811", # "redefinition of the same function" 126 | "PL", # Pylint 127 | "RUF012", # Mutable Class Attributes 128 | "UP007", # Option and Union 129 | ] 130 | 131 | [tool.ruff.lint.per-file-ignores] 132 | "__init__.py" = ["E402", "F401"] # import violations, module imported but unused 133 | "docling_mcp/servers/mcp_server.py" = ["F401"] # module inmported but unused 134 | "tests/**.py" = ["D"] # ignore flake8-docstrings in tests 135 | 136 | [tool.ruff.lint.pep8-naming] 137 | classmethod-decorators = [ 138 | # Allow Pydantic's `@validator` decorator to trigger class method treatment. 139 | "pydantic.validator", 140 | ] 141 | 142 | [tool.ruff.lint.mccabe] 143 | max-complexity = 20 144 | 145 | [tool.ruff.lint.isort.sections] 146 | "docling" = ["docling", "docling_core"] 147 | 148 | [tool.ruff.lint.isort] 149 | combine-as-imports = true 150 | section-order = [ 151 | "future", 152 | "standard-library", 153 | "third-party", 154 | "docling", 155 | "first-party", 156 | "local-folder", 157 | ] 158 | 159 | [tool.ruff.lint.pydocstyle] 160 | convention = "google" 161 | 162 | [tool.mypy] 163 | pretty = true 164 | strict = true 165 | no_implicit_optional = true 166 | plugins = "pydantic.mypy" 167 | python_version = "3.10" 168 | 169 | [[tool.mypy.overrides]] 170 | module = [ 171 | "easyocr.*", 172 | "tesserocr.*", 173 | "rapidocr_onnxruntime.*", 174 | "requests.*", 175 | ] 176 | ignore_missing_imports = true 177 | 178 | [tool.pytest.ini_options] 179 | asyncio_mode = "auto" 180 | asyncio_default_fixture_loop_scope = "function" 181 | minversion = "8.2" 182 | testpaths = [ 183 | "tests", 184 | ] 185 | addopts = "-rA --color=yes --tb=short --maxfail=5" 186 | markers = [ 187 | "asyncio", 188 | ] 189 | 190 | [tool.semantic_release] 191 | # for default values check: 192 | # https://github.com/python-semantic-release/python-semantic-release/blob/v7.32.2/semantic_release/defaults.cfg 193 | 194 | version_source = "tag_only" 195 | branch = "main" 196 | 197 | # configure types which should trigger minor and patch version bumps respectively 198 | # (note that they must be a subset of the configured allowed types): 199 | parser_angular_allowed_types = "build,chore,ci,docs,feat,fix,perf,style,refactor,test" 200 | parser_angular_minor_types = "feat" 201 | parser_angular_patch_types = "fix,perf" 202 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/docling-project/docling-mcp/9482c470741e6f18a77c5fe1150320f6edc1a737/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_generation_tools.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from docling_mcp.logger import setup_logger 4 | from docling_mcp.shared import local_document_cache 5 | from docling_mcp.tools.generation import ( # noqa: F401 6 | add_listitem_to_list_in_docling_document, 7 | add_paragraph_to_docling_document, 8 | add_section_heading_to_docling_document, 9 | add_table_in_html_format_to_docling_document, 10 | add_title_to_docling_document, 11 | close_list_in_docling_document, 12 | create_new_docling_document, 13 | export_docling_document_to_markdown, 14 | open_list_in_docling_document, 15 | save_docling_document, 16 | ) 17 | 18 | logger = setup_logger() 19 | 20 | 21 | def test_create_docling_document() -> None: 22 | reply = create_new_docling_document(prompt="test-document") 23 | key = extract_key_from_reply(reply=reply) 24 | 25 | assert key in local_document_cache 26 | 27 | 28 | def extract_key_from_reply(reply: str) -> str: 29 | match = re.search(r"document-key:\s*([a-fA-F0-9]{32})", reply) 30 | if match: 31 | return match.group(1) 32 | 33 | return "" 34 | 35 | 36 | def test_table_in_html_format_to_docling_document() -> None: 37 | reply = create_new_docling_document(prompt="test-document") 38 | key = extract_key_from_reply(reply=reply) 39 | 40 | html_table: str = "
Demographics
NameAge
John30
Jane
" 41 | 42 | reply = add_table_in_html_format_to_docling_document( 43 | document_key=key, 44 | html_table=html_table, 45 | table_captions=["Table 2: Complex demographic data with merged cells"], 46 | ) 47 | 48 | assert reply == f"Added table to a document with key: {key}" 49 | --------------------------------------------------------------------------------