<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jeremy Xiao</title>
    <description>The latest articles on DEV Community by Jeremy Xiao (@jeremyym).</description>
    <link>https://dev.to/jeremyym</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2927881%2F771e3978-2549-4a8d-8dda-5ff21b7a9e49.png</url>
      <title>DEV Community: Jeremy Xiao</title>
      <link>https://dev.to/jeremyym</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jeremyym"/>
    <language>en</language>
    <item>
      <title>MarkItDown Python and MCP Experiment Log: 14 Fixtures and a Raw STDIO Handshake</title>
      <dc:creator>Jeremy Xiao</dc:creator>
      <pubDate>Wed, 30 Sep 2026 03:50:08 +0000</pubDate>
      <link>https://dev.to/jeremyym/shi-yan-ji-lu-markitdown-python-zhi-nan-mcp-zhi-nan-2026-09-23-4an8</link>
      <guid>https://dev.to/jeremyym/shi-yan-ji-lu-markitdown-python-zhi-nan-mcp-zhi-nan-2026-09-23-4an8</guid>
      <description>&lt;p&gt;This log records the environment, commands, fixture results, and MCP handshake behind the &lt;a href="https://dev.to/jeremyym/markitdown-in-python-convert-pdf-docx-xlsx-and-more-to-markdown-and-the-one-case-it-fails-423i"&gt;Python guide&lt;/a&gt; and &lt;a href="https://dev.to/jeremyym/markitdown-mcp-setup-for-claude-desktop-cursor-and-cline-one-tool-four-uri-schemes-3imj"&gt;MCP setup guide&lt;/a&gt;. It reports one run from 2026-09-23; anything not tested is marked explicitly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run record
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Post IDs&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;python-guide&lt;/code&gt; / &lt;code&gt;mcp-guide&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run date&lt;/td&gt;
&lt;td&gt;2026-09-23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operator&lt;/td&gt;
&lt;td&gt;Commands executed with AI assistance; outputs recorded as returned (manual review pending)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Environment&lt;/td&gt;
&lt;td&gt;macOS (Darwin 26.5.1, Apple Silicon), &lt;code&gt;uv 0.10.8&lt;/code&gt;, Python 3.12 (&lt;code&gt;uv venv --python=3.12&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tools&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;markitdown 0.1.8&lt;/code&gt; (released on PyPI 2026-09-21), &lt;code&gt;markitdown-mcp 0.0.1a7&lt;/code&gt; (released on PyPI 2026-09-14)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conversion mode&lt;/td&gt;
&lt;td&gt;Local CLI &lt;code&gt;markitdown &amp;lt;file&amp;gt;&lt;/code&gt; (no LLM client, plugins, or Azure services); MCP over STDIO&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inputs&lt;/td&gt;
&lt;td&gt;14 public fixtures from &lt;code&gt;samples/quality-gallery/files/&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output path&lt;/td&gt;
&lt;td&gt;Temporary directory, not committed; key results are excerpted here and in the companion guides&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Status&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;run&lt;/code&gt;; manual review is pending&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The following installation commands were run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv venv &lt;span class="nt"&gt;--python&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3.12 mdvenv
uv pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--python&lt;/span&gt; mdvenv/bin/python &lt;span class="s1"&gt;'markitdown[all]'&lt;/span&gt; markitdown-mcp  &lt;span class="c"&gt;# initially resolved markitdown to 0.1.5&lt;/span&gt;
uv pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--python&lt;/span&gt; mdvenv/bin/python &lt;span class="nt"&gt;--prerelease&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;allow &lt;span class="s1"&gt;'markitdown[all]==0.1.8'&lt;/span&gt;  &lt;span class="c"&gt;# upgraded to 0.1.8&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One installation wrinkle: in this environment, the Azure-related extra for &lt;code&gt;markitdown[all]&lt;/code&gt; 0.1.8 depended on a prerelease package. Without prereleases enabled, the resolver fell back to &lt;code&gt;markitdown&lt;/code&gt; 0.1.5 without an error. Installing only the needed extras (for example, &lt;code&gt;markitdown[pdf,docx,xlsx,pptx]&lt;/code&gt;) or enabling prereleases avoids that resolution path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conversion results (&lt;code&gt;markitdown&lt;/code&gt; 0.1.8)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Exit&lt;/th&gt;
&lt;th&gt;Lines&lt;/th&gt;
&lt;th&gt;Chars&lt;/th&gt;
&lt;th&gt;Table rows&lt;/th&gt;
&lt;th&gt;Observation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;release-overview.pdf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;46&lt;/td&gt;
&lt;td&gt;1255&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Headings and paragraphs retained&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;q3-results.pdf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td&gt;1453&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;Pipeline table retained; numeric values matched, with blank spacer columns between value columns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;library-note.pdf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;34&lt;/td&gt;
&lt;td&gt;2556&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;Two tables; usable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;customer-research-brief.docx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;581&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Headings and lists intact; a table header cell was empty&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;q3-budget.xlsx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;582&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;Sheet names became &lt;code&gt;##&lt;/code&gt; headings; an &lt;code&gt;Unnamed: 1&lt;/code&gt; header artifact appeared&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;workshop-budget.pptx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;189&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Text only; layout was not retained&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ab-test-note.html&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;29&lt;/td&gt;
&lt;td&gt;1386&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Clean output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;urban-orchards.epub&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;td&gt;692&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Chapters became headings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;churn-cohort-notebook.ipynb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;td&gt;453&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Cell source was concatenated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;moving-averages-scan.pdf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;No text layer; output was one newline despite exit code 0 (silent failure)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;q3-results-scan.pdf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Same: one newline despite exit code 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;delivery-note-scan.pdf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Same: one newline despite exit code 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;workshop-budget-scan.png&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Only &lt;code&gt;ImageSize: 1600x1100&lt;/code&gt; metadata&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;delivery-note-scan.png&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Metadata only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For these fixtures, headings, lists, and simple tables were usable, though some tables had artifacts. Three scanned PDFs returned a single newline with exit code 0, so a successful process exit did not guarantee useful output; check output length. This run did not test LLM image descriptions, the &lt;code&gt;markitdown-ocr&lt;/code&gt; plugin, or Azure Document Intelligence / Content Understanding because credentials were unavailable. Any mention of those capabilities elsewhere is based on the official README, not on this run.&lt;/p&gt;

&lt;h2&gt;
  
  
  MCP handshake (&lt;code&gt;markitdown-mcp&lt;/code&gt; 0.0.1a7 over STDIO)
&lt;/h2&gt;

&lt;p&gt;The test wrote JSON-RPC messages to the &lt;code&gt;markitdown-mcp&lt;/code&gt; process in this order: &lt;code&gt;initialize&lt;/code&gt; → &lt;code&gt;notifications/initialized&lt;/code&gt; → &lt;code&gt;tools/list&lt;/code&gt; → &lt;code&gt;tools/call&lt;/code&gt;, then read stdout.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;initialize&lt;/code&gt; returned &lt;code&gt;serverInfo = {"name":"markitdown","version":""}&lt;/code&gt; and protocol version &lt;code&gt;2025-06-18&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tools/list&lt;/code&gt; returned one tool, &lt;code&gt;convert_to_markdown&lt;/code&gt;, with one required string parameter: &lt;code&gt;uri&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tools/call&lt;/code&gt; with &lt;code&gt;file:///…/q3-results.pdf&lt;/code&gt; returned &lt;code&gt;isError: false&lt;/code&gt; and text content containing the converted Markdown. Its first lines matched the CLI output.&lt;/li&gt;
&lt;li&gt;The package requirements recorded for this run were &lt;code&gt;markitdown[all]&amp;gt;=0.1.1,&amp;lt;0.2.0&lt;/code&gt; and &lt;code&gt;mcp&amp;gt;=2.1.1,&amp;lt;3.0.0&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Claude Desktop, Cursor, and Cline GUI configurations were not tested because those clients were not available in the environment. Their examples came from the official README and client documentation. The Docker image was not built or verified.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;AI disclosure:&lt;/strong&gt; This article was translated and prepared with AI from the experiment record captured on 2026-09-23. The experiment is reported as run; manual review has not been completed. Claude Desktop, Cursor, and Cline GUI setup and the Docker image build were not tested.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>mcp</category>
      <category>testing</category>
    </item>
    <item>
      <title>MarkItDown MCP Setup for Claude Desktop, Cursor and Cline (One Tool, Four URI Schemes)</title>
      <dc:creator>Jeremy Xiao</dc:creator>
      <pubDate>Tue, 29 Sep 2026 06:15:40 +0000</pubDate>
      <link>https://dev.to/jeremyym/markitdown-mcp-setup-for-claude-desktop-cursor-and-cline-one-tool-four-uri-schemes-3imj</link>
      <guid>https://dev.to/jeremyym/markitdown-mcp-setup-for-claude-desktop-cursor-and-cline-one-tool-four-uri-schemes-3imj</guid>
      <description>&lt;p&gt;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 &lt;code&gt;convert_to_markdown(uri)&lt;/code&gt;, accepts &lt;code&gt;http:&lt;/code&gt;, &lt;code&gt;https:&lt;/code&gt;, &lt;code&gt;file:&lt;/code&gt; and &lt;code&gt;data:&lt;/code&gt; URIs, and returned a real converted document on the first call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pip install markitdown-mcp&lt;/code&gt;, then &lt;code&gt;markitdown-mcp&lt;/code&gt; speaks STDIO; add &lt;code&gt;--http --host 127.0.0.1 --port 3001&lt;/code&gt; for Streamable HTTP and SSE.&lt;/li&gt;
&lt;li&gt;The server exposes exactly one tool with one required parameter (&lt;code&gt;uri&lt;/code&gt;), so there is nothing to learn beyond the URI schemes (verified on markitdown-mcp 0.0.1a7).&lt;/li&gt;
&lt;li&gt;It runs with the privileges of your user and has &lt;strong&gt;no authentication&lt;/strong&gt;: local use only, never bind it to a public interface.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the server actually exposes (verbatim handshake)
&lt;/h2&gt;

&lt;p&gt;Instead of paraphrasing the docs, I piped a JSON-RPC handshake into the server on 2026-09-23 and read the raw replies. After &lt;code&gt;initialize&lt;/code&gt;, the server identifies itself as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"markitdown"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The empty &lt;code&gt;version&lt;/code&gt; string is real, not a copy-paste mistake — don't waste time wondering why it is missing. &lt;code&gt;tools/list&lt;/code&gt; then returns a single tool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"convert_to_markdown"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Convert a resource described by an http:, https:, file: or data: URI to markdown"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"inputSchema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"uri"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Uri"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"uri"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;tools/call&lt;/code&gt; with a &lt;code&gt;file://&lt;/code&gt; URI pointing at a local PDF returned the converted Markdown in &lt;code&gt;content[0].text&lt;/code&gt; with &lt;code&gt;isError: false&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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%  |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole contract: one tool, one string parameter, Markdown back. The interesting part is what the URI scheme means in practice — &lt;code&gt;file:&lt;/code&gt; reads from the machine running the server, &lt;code&gt;https:&lt;/code&gt; lets the model pull a web page and convert it, and &lt;code&gt;data:&lt;/code&gt; accepts inline base64.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install and pick a transport
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;markitdown-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default transport is STDIO, which is what most desktop clients want:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;markitdown-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;markitdown-mcp &lt;span class="nt"&gt;--http&lt;/span&gt; &lt;span class="nt"&gt;--host&lt;/span&gt; 127.0.0.1 &lt;span class="nt"&gt;--port&lt;/span&gt; 3001
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In HTTP mode, Streamable HTTP lives at &lt;code&gt;/mcp&lt;/code&gt; and the older SSE transport at &lt;code&gt;/sse&lt;/code&gt;. Keep the host at &lt;code&gt;127.0.0.1&lt;/code&gt;; the project's README is explicit that this server is meant for local use with trusted agents.&lt;/p&gt;

&lt;p&gt;One environment note from my run: &lt;code&gt;markitdown-mcp&lt;/code&gt; depends on &lt;code&gt;markitdown[all]&lt;/code&gt; (&lt;code&gt;&amp;gt;=0.1.1,&amp;lt;0.2.0&lt;/code&gt;). Installing it into an environment that already has a pre-release-sensitive &lt;code&gt;markitdown[all]&lt;/code&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Claude Desktop: the config that works
&lt;/h2&gt;

&lt;p&gt;Open &lt;code&gt;claude_desktop_config.json&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;macOS: &lt;code&gt;~/Library/Application Support/Claude/claude_desktop_config.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Windows: &lt;code&gt;%APPDATA%\Claude\claude_desktop_config.json&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The official README recommends running the server through Docker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"markitdown"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"docker"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"run"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"--rm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"-i"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"markitdown-mcp:latest"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To let it read files from a local folder, mount it and use paths inside the container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"markitdown"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"docker"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"run"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"--rm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"-i"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"-v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/Users/me/Documents/contracts:/workdir"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"markitdown-mcp:latest"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then reference &lt;code&gt;file:///workdir/contract-2026.pdf&lt;/code&gt; in chat. If you installed the package with &lt;code&gt;pip&lt;/code&gt; and the command is on your PATH, the same entry works without Docker — replace the &lt;code&gt;command&lt;/code&gt;/&lt;code&gt;args&lt;/code&gt; pair with &lt;code&gt;"command": "markitdown-mcp"&lt;/code&gt;. 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.&lt;/p&gt;

&lt;p&gt;Restart Claude Desktop after the edit, then ask it to list its tools before debugging anything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cursor and Cline: same idea, different file
&lt;/h2&gt;

&lt;p&gt;Cursor reads MCP servers from &lt;code&gt;~/.cursor/mcp.json&lt;/code&gt; (global) or &lt;code&gt;.cursor/mcp.json&lt;/code&gt; in a project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"markitdown"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"markitdown-mcp"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart Cursor, open the MCP settings panel, and confirm the &lt;code&gt;markitdown&lt;/code&gt; server shows one tool.&lt;/p&gt;

&lt;p&gt;Cline (the VS Code extension) manages the same &lt;code&gt;mcpServers&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;The pattern across all three clients is identical — one &lt;code&gt;command&lt;/code&gt;, optional &lt;code&gt;args&lt;/code&gt; — which is the actual argument for using MCP here instead of a custom script per assistant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Docker, and the Inspector when it breaks
&lt;/h2&gt;

&lt;p&gt;Building the image yourself makes the Claude Desktop setup above work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/microsoft/markitdown.git
&lt;span class="nb"&gt;cd &lt;/span&gt;markitdown/packages/markitdown-mcp
docker build &lt;span class="nt"&gt;-t&lt;/span&gt; markitdown-mcp:latest &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For debugging, the MCP Inspector is the fastest way to see whether the problem is your client or your server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @modelcontextprotocol/inspector
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the Inspector, pick the transport, connect, then open &lt;strong&gt;Tools → List Tools → convert_to_markdown&lt;/strong&gt; and run it on a URI:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Transport&lt;/th&gt;
&lt;th&gt;Connect to&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;STDIO&lt;/td&gt;
&lt;td&gt;command &lt;code&gt;markitdown-mcp&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Streamable HTTP&lt;/td&gt;
&lt;td&gt;&lt;code&gt;http://127.0.0.1:3001/mcp&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSE&lt;/td&gt;
&lt;td&gt;&lt;code&gt;http://127.0.0.1:3001/sse&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Common failures I'd check first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tool never appears&lt;/strong&gt; — the server process exited on start. Run the exact &lt;code&gt;command&lt;/code&gt; from your config in a terminal; a missing binary or a wrong Python environment shows up immediately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"File not found" for a path you can see&lt;/strong&gt; — &lt;code&gt;file:&lt;/code&gt; URIs resolve on the server's machine and process, not in the chat. With Docker, that means the mounted path (&lt;code&gt;/workdir/...&lt;/code&gt;), not your host path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It works locally, fails in a container&lt;/strong&gt; — 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Random protocol errors&lt;/strong&gt; — something printed to stdout. STDIO transport uses stdout for JSON-RPC; a stray &lt;code&gt;print()&lt;/code&gt; breaks the stream. Send logs to stderr or a file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Large files time out&lt;/strong&gt; — conversion is synchronous; keep an eye on file size and client timeouts before wiring this into anything interactive.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Security: the one rule, and what to do without a server
&lt;/h2&gt;

&lt;p&gt;The README is blunt about this, and it deserves repeating verbatim: the server &lt;strong&gt;does not support authentication&lt;/strong&gt;, runs with the privileges of the user who started it, and &lt;code&gt;convert_to_markdown&lt;/code&gt; can read any file that user can access — or fetch any URL the machine can reach. Bind it to &lt;code&gt;localhost&lt;/code&gt;, keep it out of shared networks, and if you need more isolation, run it in a container or VM with a restricted mount.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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 — &lt;a href="https://markitdown.tech/markitdown-mcp?utm_source=devto&amp;amp;utm_medium=community&amp;amp;utm_campaign=devto-series&amp;amp;utm_content=mcp-guide" rel="noopener noreferrer"&gt;markitdown.tech&lt;/a&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;AI disclosure:&lt;/strong&gt; 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.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>markdown</category>
      <category>tooling</category>
    </item>
    <item>
      <title>MarkItDown in Python: Convert PDF, DOCX, XLSX and More to Markdown (and the One Case It Fails Silently)</title>
      <dc:creator>Jeremy Xiao</dc:creator>
      <pubDate>Mon, 28 Sep 2026 06:20:29 +0000</pubDate>
      <link>https://dev.to/jeremyym/markitdown-in-python-convert-pdf-docx-xlsx-and-more-to-markdown-and-the-one-case-it-fails-423i</link>
      <guid>https://dev.to/jeremyym/markitdown-in-python-convert-pdf-docx-xlsx-and-more-to-markdown-and-the-one-case-it-fails-423i</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4a08nrlh6ot6jqj2y0kw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4a08nrlh6ot6jqj2y0kw.png" alt=" " width="800" height="337"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;MarkItDown turns PDF, Word, Excel, PowerPoint, HTML, EPUB and more into Markdown with one Python call, and it is designed for LLM pipelines rather than pixel-perfect documents. I ran it against 14 fixtures to find its edges: the sharpest one is that scanned PDFs return a single newline with exit code 0 — a silent failure you have to check for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pip install 'markitdown[all]'&lt;/code&gt; and &lt;code&gt;markitdown report.pdf -o report.md&lt;/code&gt; is the entire CLI.&lt;/li&gt;
&lt;li&gt;The Python API is three lines; batching a folder is one &lt;code&gt;for&lt;/code&gt; loop — code below, tested on markitdown 0.1.8 / Python 3.12.&lt;/li&gt;
&lt;li&gt;Text-based files convert cleanly, tables often need light cleanup, and &lt;strong&gt;scanned PDFs produce empty output without raising an error&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What MarkItDown does (and the one thing it does not)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/microsoft/markitdown" rel="noopener noreferrer"&gt;MarkItDown&lt;/a&gt; is Microsoft's MIT-licensed Python utility for converting files to Markdown "for use with LLMs and related text analysis pipelines." It preserves headings, lists, tables and links instead of flattening everything into plain text, which is exactly what you want before chunking a document into a retrieval index or pasting it into a prompt.&lt;/p&gt;

&lt;p&gt;Two honest boundaries from the project's own README:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It is built for &lt;strong&gt;machine consumption&lt;/strong&gt;, not high-fidelity conversion for humans. The output is usually presentable, but layout fidelity is not the goal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OCR is not part of the default pipeline.&lt;/strong&gt; A scanned PDF has no text layer, and MarkItDown will happily return almost nothing. I'll show that failure below, plus three ways to handle it.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Install and run in five minutes
&lt;/h2&gt;

&lt;p&gt;MarkItDown 0.1.8 requires Python 3.10–3.14. A virtual environment keeps the format dependencies out of your system Python:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv
&lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate        &lt;span class="c"&gt;# Windows: .venv\Scripts\activate&lt;/span&gt;
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s1"&gt;'markitdown[all]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you only need a couple of formats, install the specific extras instead — this also avoids a resolver wrinkle I hit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s1"&gt;'markitdown[pdf,docx,xlsx,pptx]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;A small wrinkle worth knowing:&lt;/strong&gt; &lt;code&gt;markitdown[all]&lt;/code&gt; at 0.1.8 pulls an Azure extra that currently depends on a pre-release package. On my machine that made the resolver fall back to 0.1.5 without saying much; installing the extras you actually use (or adding &lt;code&gt;--pre&lt;/code&gt;) gets you 0.1.8. If a version-sensitive feature is missing, check &lt;code&gt;python -c "import importlib.metadata as m; print(m.version('markitdown'))"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then convert something:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;markitdown report.pdf &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; report.md        &lt;span class="c"&gt;# to stdout&lt;/span&gt;
markitdown report.pdf &lt;span class="nt"&gt;-o&lt;/span&gt; report.md       &lt;span class="c"&gt;# to a file&lt;/span&gt;
&lt;span class="nb"&gt;cat &lt;/span&gt;report.pdf | markitdown &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; report.md  &lt;span class="c"&gt;# from a pipe&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Python API: one file, then a whole folder
&lt;/h2&gt;

&lt;p&gt;The basic call is three lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;markitdown&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MarkItDown&lt;/span&gt;

&lt;span class="n"&gt;md&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MarkItDown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;enable_plugins&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;q3-results.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;markdown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because &lt;code&gt;convert()&lt;/code&gt; is just a function, batching a folder is a loop. This is the script I actually want in every RAG project I start:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;markitdown&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MarkItDown&lt;/span&gt;

&lt;span class="n"&gt;md&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MarkItDown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;enable_plugins&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;markdown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exist_ok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;formats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.docx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.pptx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.xlsx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.epub&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rglob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;suffix&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;formats&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="n"&gt;dest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stem&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;markdown&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="c1"&gt;# the scanned-PDF guard
&lt;/span&gt;            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EMPTY?  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; -&amp;gt; check for a text layer or add OCR&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;dest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok     &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;dest&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fail   &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;len(text.strip()) &amp;lt; 20&lt;/code&gt; guard is not paranoia. Without it, three of my fixtures would have been written to disk as a file containing one newline, and nothing in the pipeline would have complained.&lt;/p&gt;

&lt;h2&gt;
  
  
  I converted 14 fixtures: here is what actually came out
&lt;/h2&gt;

&lt;p&gt;I ran &lt;code&gt;markitdown 0.1.8&lt;/code&gt; on Python 3.12 over a set of public sample files (text PDFs, scanned PDFs, DOCX, XLSX, PPTX, HTML, EPUB, a notebook and two PNGs). Same command for every file: &lt;code&gt;markitdown &amp;lt;file&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;th&gt;Table rows&lt;/th&gt;
&lt;th&gt;What I saw&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;release-overview.pdf&lt;/td&gt;
&lt;td&gt;46 lines / 1,255 chars&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Clean prose, headings preserved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;q3-results.pdf&lt;/td&gt;
&lt;td&gt;23 lines / 1,453 chars&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;Pipe tables, a few empty spacer columns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;library-note.pdf&lt;/td&gt;
&lt;td&gt;34 lines / 2,556 chars&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;Two tables, readable, minor cell drift&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;customer-research-brief.docx&lt;/td&gt;
&lt;td&gt;22 lines / 581 chars&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Headings and bullets perfect; table header cells empty&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;q3-budget.xlsx&lt;/td&gt;
&lt;td&gt;18 lines / 582 chars&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;Sheet name became &lt;code&gt;##&lt;/code&gt;, values as a table, one &lt;code&gt;Unnamed: 1&lt;/code&gt; header artifact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;workshop-budget.pptx&lt;/td&gt;
&lt;td&gt;7 lines / 189 chars&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Slide text only; no layout detail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ab-test-note.html&lt;/td&gt;
&lt;td&gt;29 lines / 1,386 chars&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Clean&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;urban-orchards.epub&lt;/td&gt;
&lt;td&gt;28 lines / 692 chars&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Chapters as headings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;churn-cohort-notebook.ipynb&lt;/td&gt;
&lt;td&gt;24 lines / 453 chars&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Cell sources concatenated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;moving-averages-scan.pdf&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1 line / 1 char&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;No text layer — silent empty output&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;q3-results-scan.pdf&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1 line / 1 char&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;delivery-note-scan.pdf&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1 line / 1 char&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;workshop-budget-scan.png&lt;/td&gt;
&lt;td&gt;2 lines / 22 chars&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ImageSize: 1600x1100&lt;/code&gt; only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;delivery-note-scan.png&lt;/td&gt;
&lt;td&gt;2 lines / 21 chars&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;EXIF-style metadata only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two outputs are worth reading in full, because they show the difference between "works" and "works without cleanup."&lt;/p&gt;

&lt;p&gt;A text PDF with tables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;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%  |
| Power tools     | $2,875k |     | $3,010k |     | -4.5%  |
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The numbers are right, but the empty columns between value pairs are a PDF layout artifact — a downstream parser will see phantom cells. Budget two minutes per table for cleanup, or normalize columns before indexing.&lt;/p&gt;

&lt;p&gt;A DOCX where the structure survives but one table does not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Customer Research Brief&lt;/span&gt;
Prepared for the August product review.
&lt;span class="gu"&gt;## Key Findings&lt;/span&gt;
&lt;span class="p"&gt;*&lt;/span&gt; Teams want Markdown exports that preserve headings and tables.
&lt;span class="p"&gt;*&lt;/span&gt; Researchers paste converted notes into LLM prompts before archiving.
&lt;span class="gu"&gt;## Interview Summary&lt;/span&gt;
|  |  |  |
| --- | --- | --- |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The headings and bullets are exactly as authored. The table lost its header text. This is the pattern to expect: &lt;strong&gt;structure survives, complex table furniture does not&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scanned files: three ways to handle them
&lt;/h2&gt;

&lt;p&gt;Everything above exits with code 0, including the empty scans — so decide based on the file, not the exit status.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;LLM image descriptions&lt;/strong&gt; (for images and image-heavy PPTX). Pass an OpenAI-compatible client and model, and MarkItDown will describe images instead of skipping them:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;   &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;markitdown&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MarkItDown&lt;/span&gt;
   &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

   &lt;span class="n"&gt;md&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MarkItDown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;llm_client&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;llm_model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;workshop-budget.pptx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;markitdown-ocr&lt;/code&gt; plugin&lt;/strong&gt; (for scanned PDF, DOCX, PPTX or XLSX). It adds OCR over embedded images using the same &lt;code&gt;llm_client&lt;/code&gt; pattern — no extra ML dependencies, but it does need an API key:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   pip &lt;span class="nb"&gt;install &lt;/span&gt;markitdown-ocr openai
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;   &lt;span class="n"&gt;md&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MarkItDown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;enable_plugins&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;llm_client&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;llm_model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;q3-results-scan.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A cloud analyzer&lt;/strong&gt; if you are already on Azure: &lt;code&gt;pip install 'markitdown[az-doc-intel]'&lt;/code&gt; then &lt;code&gt;markitdown scan.pdf -d -e "&amp;lt;endpoint&amp;gt;"&lt;/code&gt;, or the newer Content Understanding path with &lt;code&gt;--use-cu&lt;/code&gt; for layout analysis, structured fields and even audio/video.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If none of those fit — no API key, one-off file, or a non-technical teammate — the same conversion runs without any local setup on &lt;a href="https://markitdown.tech/microsoft-markitdown?utm_source=devto&amp;amp;utm_medium=community&amp;amp;utm_campaign=devto-series&amp;amp;utm_content=python-guide" rel="noopener noreferrer"&gt;markitdown.tech&lt;/a&gt;, an independent hosted front end built on the open-source library (I maintain it). Its browser path handles common formats locally, server mode covers the formats that need a backend, and scanned pages go through an OCR mode. Treat it as the "skip the venv" option, not a replacement for the library in a pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production notes and when to skip Python
&lt;/h2&gt;

&lt;p&gt;If the file might come from an untrusted user, read the &lt;strong&gt;Security Considerations&lt;/strong&gt; section of the README before you ship. The short version: &lt;code&gt;convert()&lt;/code&gt; performs I/O with the privileges of your process and accepts local paths, URLs and byte streams. In a server, sanitize inputs and call the narrowest function that fits — &lt;code&gt;convert_local()&lt;/code&gt; for local files, &lt;code&gt;convert_stream()&lt;/code&gt; when you already opened the bytes, or &lt;code&gt;convert_response()&lt;/code&gt; for HTTP you fetched yourself.&lt;/p&gt;

&lt;p&gt;When to use something else entirely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One-off scans with no API key&lt;/strong&gt; — the hosted OCR path above is faster than assembling one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pixel-perfect Word/PDF fidelity for humans&lt;/strong&gt; — that is not MarkItDown's job; use a converter built for layout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A watched folder that must run unattended&lt;/strong&gt; — wrap the batch script with the empty-output guard and a retry queue, or you will silently index blank documents.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you run the batch script on your own corpus: what breaks first — the tables, the reading order, or the scans? I collect the weird cases and test them publicly.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;AI assistance disclosure: this article was drafted with AI assistance. Every command, version and output in it was verified against markitdown 0.1.8 and a local fixture run on 2026-09-23.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>markdown</category>
      <category>python</category>
      <category>ai</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I Treated Buying a Robot Vacuum Like Choosing a Tech Stack</title>
      <dc:creator>Jeremy Xiao</dc:creator>
      <pubDate>Sat, 06 Jun 2026 08:43:12 +0000</pubDate>
      <link>https://dev.to/jeremyym/i-treated-buying-a-robot-vacuum-like-choosing-a-tech-stack-34fl</link>
      <guid>https://dev.to/jeremyym/i-treated-buying-a-robot-vacuum-like-choosing-a-tech-stack-34fl</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgvr62ksee0cbgc2v2kau.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgvr62ksee0cbgc2v2kau.png" alt=" " width="799" height="410"&gt;&lt;/a&gt;&lt;br&gt;
A few weeks ago, I went looking for a robot vacuum and realized I was making the same mistake I have made with software tools: I was comparing headline features instead of real-world fit. That is what led me to build and use &lt;a href="https://bestrobotvacuum.io" rel="noopener noreferrer"&gt;best robot vacuum&lt;/a&gt;, a comparison site designed to make robot vacuum shopping feel less like reading spec sheets and more like making a grounded engineering decision.&lt;/p&gt;

&lt;p&gt;At first, I thought the problem was simple.&lt;/p&gt;

&lt;p&gt;Find the model with the strongest suction.&lt;/p&gt;

&lt;p&gt;Buy it.&lt;/p&gt;

&lt;p&gt;Done.&lt;/p&gt;

&lt;p&gt;That is the consumer electronics version of “pick the framework with the most GitHub stars.”&lt;/p&gt;

&lt;p&gt;It feels objective. It is easy to explain. It gives you a number to point at.&lt;/p&gt;

&lt;p&gt;But there is a trap.&lt;/p&gt;

&lt;p&gt;A robot vacuum is not a single metric product. It is a system that operates inside another system: your home. And your home has constraints.&lt;/p&gt;

&lt;p&gt;Carpet behaves differently from hardwood. Pet hair behaves differently from dust. A small apartment has different navigation needs than a multi-room house. A home with kids has different obstacle problems than a minimalist studio. A person who wants weekly automation has different tolerance for maintenance than someone who just wants help between deep cleans.&lt;/p&gt;

&lt;p&gt;That is when the buying process started to feel strangely familiar.&lt;/p&gt;

&lt;p&gt;It was not like choosing a gadget.&lt;/p&gt;

&lt;p&gt;It was like choosing infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The spec sheet illusion
&lt;/h2&gt;

&lt;p&gt;Robot vacuum listings love numbers.&lt;/p&gt;

&lt;p&gt;You will see suction power, battery life, dock capacity, mop temperature, obstacle clearance, noise level, mapping type, water tank size, and dozens of marketing terms that sound important.&lt;/p&gt;

&lt;p&gt;Some of them are important.&lt;/p&gt;

&lt;p&gt;Many of them are only important in context.&lt;/p&gt;

&lt;p&gt;A model with huge suction might still be annoying if the brush tangles every two days. A vacuum with a beautiful self-cleaning dock might be overkill if you live in a small hard-floor apartment. A vacuum-mop combo might look advanced but become risky if it cannot reliably detect rugs.&lt;/p&gt;

&lt;p&gt;This is the same failure mode we see in technical decisions.&lt;/p&gt;

&lt;p&gt;A database benchmark looks great until your workload is write-heavy.&lt;/p&gt;

&lt;p&gt;A JavaScript framework feels elegant until your team has to maintain it for three years.&lt;/p&gt;

&lt;p&gt;An AI coding tool looks magical until the generated code starts accumulating silent complexity.&lt;/p&gt;

&lt;p&gt;The visible layer is easy to compare.&lt;/p&gt;

&lt;p&gt;The operational layer is where the real decision lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I wanted a comparison site that thinks in use cases
&lt;/h2&gt;

&lt;p&gt;The goal of Best Robot Vacuum is not to say “this is the one perfect vacuum.”&lt;/p&gt;

&lt;p&gt;That would be dishonest.&lt;/p&gt;

&lt;p&gt;The goal is to help people narrow the field based on actual needs.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do you have pets?&lt;/li&gt;
&lt;li&gt;Do you have carpet?&lt;/li&gt;
&lt;li&gt;Do you need mopping?&lt;/li&gt;
&lt;li&gt;Do you care about self-emptying?&lt;/li&gt;
&lt;li&gt;Do you want obstacle avoidance?&lt;/li&gt;
&lt;li&gt;Do you want the lowest maintenance setup?&lt;/li&gt;
&lt;li&gt;Do you have a realistic budget?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those questions matter more than most people expect.&lt;/p&gt;

&lt;p&gt;If someone has two shedding dogs, they should not start with the same shortlist as someone living alone with tile floors. That is why a focused page like &lt;a href="https://bestrobotvacuum.io/best-robot-vacuum-for-pet-hair" rel="noopener noreferrer"&gt;robot vacuums for pet hair&lt;/a&gt; is more useful than a generic top ten list. The problem is not “what is the best vacuum?” The problem is “what is the best vacuum for this environment?”&lt;/p&gt;

&lt;p&gt;That difference sounds small.&lt;/p&gt;

&lt;p&gt;It changes everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The underrated feature: maintainability
&lt;/h2&gt;

&lt;p&gt;When developers evaluate tools, we eventually ask the maintenance question.&lt;/p&gt;

&lt;p&gt;Who owns this?&lt;/p&gt;

&lt;p&gt;How painful is the upgrade path?&lt;/p&gt;

&lt;p&gt;What breaks when the happy path ends?&lt;/p&gt;

&lt;p&gt;Robot vacuums have the same issue.&lt;/p&gt;

&lt;p&gt;A vacuum that cleans well but requires constant brush cleaning may not save much time. A model with a tiny dustbin may be fine on paper but annoying in a pet-heavy home. A mop system that needs manual washing every run may be a bad fit for someone who wants automation, not another chore.&lt;/p&gt;

&lt;p&gt;The best robot vacuum is not the one that performs well once.&lt;/p&gt;

&lt;p&gt;It is the one that performs well repeatedly with a level of maintenance you can actually tolerate.&lt;/p&gt;

&lt;p&gt;That is a product design question as much as a hardware question.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I look for now
&lt;/h2&gt;

&lt;p&gt;After comparing enough models, I stopped asking “which robot vacuum is best?” and started asking a more useful set of questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What type of mess is this home solving for?&lt;/li&gt;
&lt;li&gt;What floor surfaces does the robot need to handle?&lt;/li&gt;
&lt;li&gt;How often will the owner realistically maintain it?&lt;/li&gt;
&lt;li&gt;Does the dock reduce work or just add complexity?&lt;/li&gt;
&lt;li&gt;Are the advanced features useful or just impressive?&lt;/li&gt;
&lt;li&gt;What tradeoff is the buyer actually making?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last question is the important one.&lt;/p&gt;

&lt;p&gt;Every robot vacuum has a tradeoff.&lt;/p&gt;

&lt;p&gt;Some are great for carpet but expensive. Some are strong for pet hair but less elegant at mopping. Some are affordable but need more manual emptying. Some are packed with automation but take up more floor space.&lt;/p&gt;

&lt;p&gt;A useful comparison tool should make those tradeoffs visible.&lt;/p&gt;

&lt;p&gt;That is why I like having side-by-side pages such as the &lt;a href="https://bestrobotvacuum.io/best-robot-vacuum-comparison-table" rel="noopener noreferrer"&gt;robot vacuum comparison table&lt;/a&gt;. Tables are not exciting, but they are honest. They force the product conversation out of vague adjectives and into concrete differences.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bigger lesson
&lt;/h2&gt;

&lt;p&gt;This project reminded me that consumer search is often broken in the same way developer tooling content is broken.&lt;/p&gt;

&lt;p&gt;Too many pages optimize for rankings.&lt;/p&gt;

&lt;p&gt;Not enough pages optimize for decisions.&lt;/p&gt;

&lt;p&gt;A good buying guide should not just answer a keyword. It should reduce uncertainty. It should help the reader understand what matters, what does not, and what depends on their situation.&lt;/p&gt;

&lt;p&gt;That is the standard I want Best Robot Vacuum to move toward.&lt;/p&gt;

&lt;p&gt;Not hype.&lt;/p&gt;

&lt;p&gt;Not sponsored ranking noise.&lt;/p&gt;

&lt;p&gt;Not “here are ten products and every one is amazing.”&lt;/p&gt;

&lt;p&gt;Just structured comparison, practical filters, and use-case-first recommendations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;If you are a developer, you already have the mental model for buying smarter hardware.&lt;/p&gt;

&lt;p&gt;Do not trust the headline metric.&lt;/p&gt;

&lt;p&gt;Look for constraints.&lt;/p&gt;

&lt;p&gt;Map the use case.&lt;/p&gt;

&lt;p&gt;Consider maintenance.&lt;/p&gt;

&lt;p&gt;Watch for hidden operational costs.&lt;/p&gt;

&lt;p&gt;And when every product claims to be the best, ask the question we ask in engineering all the time:&lt;/p&gt;

&lt;p&gt;Best for what?&lt;/p&gt;

</description>
    </item>
    <item>
      <title>🌟 Discover Your Perfect Chinese Name with Our Expert Generator 🌟</title>
      <dc:creator>Jeremy Xiao</dc:creator>
      <pubDate>Thu, 20 Mar 2025 10:49:15 +0000</pubDate>
      <link>https://dev.to/jeremyym/discover-your-perfect-chinese-name-with-our-expert-generator-4hj4</link>
      <guid>https://dev.to/jeremyym/discover-your-perfect-chinese-name-with-our-expert-generator-4hj4</guid>
      <description>&lt;p&gt;Looking for a culturally rich and meaningful Chinese name that truly resonates with you? At &lt;a href="https://chinese-name-generator.com" rel="noopener noreferrer"&gt;Chinese-Name-Generator.com&lt;/a&gt;, we blend tradition, linguistics, and personalization to craft names that honor Chinese heritage while reflecting your unique identity.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Choose Us?
&lt;/h3&gt;

&lt;p&gt;✅ Tailored Name Creation&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Phonetic Adaptation: Convert your English name into a harmonious Chinese equivalent.&lt;/li&gt;
&lt;li&gt;Meaning-Driven Options: Select names based on virtues, traits, or aspirations you value most.&lt;/li&gt;
&lt;li&gt;Custom Preferences: Specify gender, birth year (for zodiac alignment), and even choose your own surname!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;✅ In-Depth Cultural Insights Every name comes with a detailed analysis:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Complete Overview: Understand the name’s essence at a glance.&lt;/li&gt;
&lt;li&gt;Surname Significance: Learn the history and cultural weight behind your chosen family name.&lt;/li&gt;
&lt;li&gt;Character Breakdown: Explore the meaning, stroke order, and symbolism of each character.&lt;/li&gt;
&lt;li&gt;Poetic &amp;amp; Phonetic Quality: Discover how the name flows in tone and rhythm, with text-to-speech audio for perfect pronunciation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;✅ Extra Features for a Seamless Experience&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Save &amp;amp; Share: Bookmark favorites or proudly share your name on social media.&lt;/li&gt;
&lt;li&gt;Name Cards: Download elegant, customizable images to introduce your new identity.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whether you’re studying Chinese, connecting with heritage, or seeking a meaningful alias, our tool ensures your name carries depth, beauty, and cultural authenticity.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>socialmedia</category>
      <category>deepseek</category>
      <category>tiktok</category>
    </item>
  </channel>
</rss>
