MarkItDown MCP gives any MCP client — Claude Desktop, Cursor, Cline — one tool that converts a file, a URL or inline data into Markdown. I ran the server and captured the exact handshake: it exposes convert_to_markdown(uri), accepts http:, https:, file: and data: URIs, and returned a real converted document on the first call.
TL;DR
-
pip install markitdown-mcp, thenmarkitdown-mcpspeaks STDIO; add--http --host 127.0.0.1 --port 3001for Streamable HTTP and SSE. - The server exposes exactly one tool with one required parameter (
uri), so there is nothing to learn beyond the URI schemes (verified on markitdown-mcp 0.0.1a7). - It runs with the privileges of your user and has no authentication: local use only, never bind it to a public interface.
What the server actually exposes (verbatim handshake)
Instead of paraphrasing the docs, I piped a JSON-RPC handshake into the server on 2026-09-23 and read the raw replies. After initialize, the server identifies itself as:
{"name": "markitdown", "version": ""}
The empty version string is real, not a copy-paste mistake — don't waste time wondering why it is missing. tools/list then returns a single tool:
{
"name": "convert_to_markdown",
"description": "Convert a resource described by an http:, https:, file: or data: URI to markdown",
"inputSchema": {
"properties": { "uri": { "title": "Uri", "type": "string" } },
"required": ["uri"],
"type": "object"
}
}
A tools/call with a file:// URI pointing at a local PDF returned the converted Markdown in content[0].text with isError: false:
Northwind Tooling Co. — Q3 2026 Results
Unaudited condensed summary, prepared for the quarterly review
Revenue by Segment
| Segment | Q3 2026 | | Q2 2026 | | QoQ |
| --------------- | ------- | --- | ------- | --- | ------ |
| Hand tools | $4,210k | | $3,955k | | +6.4% |
That is the whole contract: one tool, one string parameter, Markdown back. The interesting part is what the URI scheme means in practice — file: reads from the machine running the server, https: lets the model pull a web page and convert it, and data: accepts inline base64.
Install and pick a transport
pip install markitdown-mcp
The default transport is STDIO, which is what most desktop clients want:
markitdown-mcp
If you prefer a long-running server that clients connect to over HTTP (for example when several tools share one process, or you want a stable URL for debugging):
markitdown-mcp --http --host 127.0.0.1 --port 3001
In HTTP mode, Streamable HTTP lives at /mcp and the older SSE transport at /sse. Keep the host at 127.0.0.1; the project's README is explicit that this server is meant for local use with trusted agents.
One environment note from my run: markitdown-mcp depends on markitdown[all] (>=0.1.1,<0.2.0). Installing it into an environment that already has a pre-release-sensitive markitdown[all] can resolve the library to an older 0.1.x — if a version-specific feature is missing, check the library version before blaming the MCP layer. A dedicated virtual environment per server avoids the question entirely.
Claude Desktop: the config that works
Open claude_desktop_config.json:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
The official README recommends running the server through Docker:
{
"mcpServers": {
"markitdown": {
"command": "docker",
"args": ["run", "--rm", "-i", "markitdown-mcp:latest"]
}
}
}
To let it read files from a local folder, mount it and use paths inside the container:
{
"mcpServers": {
"markitdown": {
"command": "docker",
"args": [
"run", "--rm", "-i",
"-v", "/Users/me/Documents/contracts:/workdir",
"markitdown-mcp:latest"
]
}
}
}
Then reference file:///workdir/contract-2026.pdf in chat. If you installed the package with pip and the command is on your PATH, the same entry works without Docker — replace the command/args pair with "command": "markitdown-mcp". Use an absolute path if the client launches with a different PATH than your shell; that single difference explains most "server failed to start" reports.
Restart Claude Desktop after the edit, then ask it to list its tools before debugging anything else.
Cursor and Cline: same idea, different file
Cursor reads MCP servers from ~/.cursor/mcp.json (global) or .cursor/mcp.json in a project:
{
"mcpServers": {
"markitdown": { "command": "markitdown-mcp" }
}
}
Restart Cursor, open the MCP settings panel, and confirm the markitdown server shows one tool.
Cline (the VS Code extension) manages the same mcpServers shape through its own MCP Servers panel: open it, choose "Configure", and paste the JSON above. Cline adds a few fields of its own around it (approval toggles), so let the extension write the file rather than editing it by hand.
The pattern across all three clients is identical — one command, optional args — which is the actual argument for using MCP here instead of a custom script per assistant.
Docker, and the Inspector when it breaks
Building the image yourself makes the Claude Desktop setup above work:
git clone https://github.com/microsoft/markitdown.git
cd markitdown/packages/markitdown-mcp
docker build -t markitdown-mcp:latest .
For debugging, the MCP Inspector is the fastest way to see whether the problem is your client or your server:
npx @modelcontextprotocol/inspector
In the Inspector, pick the transport, connect, then open Tools → List Tools → convert_to_markdown and run it on a URI:
| Transport | Connect to |
|---|---|
| STDIO | command markitdown-mcp
|
| Streamable HTTP | http://127.0.0.1:3001/mcp |
| SSE | http://127.0.0.1:3001/sse |
Common failures I'd check first:
-
Tool never appears — the server process exited on start. Run the exact
commandfrom your config in a terminal; a missing binary or a wrong Python environment shows up immediately. -
"File not found" for a path you can see —
file:URIs resolve on the server's machine and process, not in the chat. With Docker, that means the mounted path (/workdir/...), not your host path. - It works locally, fails in a container — the container cannot see the file, or the path differs between host and container. Mount the directory explicitly; do not rely on the working directory.
-
Random protocol errors — something printed to stdout. STDIO transport uses stdout for JSON-RPC; a stray
print()breaks the stream. Send logs to stderr or a file. - Large files time out — conversion is synchronous; keep an eye on file size and client timeouts before wiring this into anything interactive.
Security: the one rule, and what to do without a server
The README is blunt about this, and it deserves repeating verbatim: the server does not support authentication, runs with the privileges of the user who started it, and convert_to_markdown can read any file that user can access — or fetch any URL the machine can reach. Bind it to localhost, keep it out of shared networks, and if you need more isolation, run it in a container or VM with a restricted mount.
Also note what the tool is not: it converts, it does not review. If you point it at an internal URL, the model on the other end will happily summarize whatever comes back. Treat the URI list as part of your prompt security, not just a configuration detail.
If you do not want to run a server at all — one-off conversions, a locked-down work laptop, or teammates who do not want a Python environment — markitdown.tech runs the same conversion as a hosted tool (an independent project I maintain, built on the open-source library), with a paste-into-chat workflow for Claude and an MCP quickstart page if you want to compare configs before installing anything.
If you have this wired into an assistant already: what did you have to change first — the config, the file paths, or the timeouts? Those are the three that bit me.
AI disclosure: AI was used to draft this article from the experiment log and public documentation. The STDIO handshake, tool schema, and conversion call were captured in a 2026-09-23 test run. The Claude Desktop, Cursor, and Cline GUI setup steps and Docker image build were not tested.
Top comments (1)
The localhost only rule is the detail I would put in bold. Since this server can read any file the launching user can access and fetch internal URLs, a container with a narrow read only mount feels like the safer default rather than an optional hardening step. I also like the stdout warning because one stray debug print can make an MCP setup look mysteriously broken.