Cross-posting a technical tutorial is easy to start and surprisingly easy to get wrong. A URL import can leave code blocks split, headings as plain text, or metadata incomplete. The result may look acceptable at a glance while damaging the parts readers need most.
This tutorial shows a reviewable DEV.to to Medium workflow using publish-agents, an open-source TypeScript project by Fernando Paladini. Its medium-publisher package imports a public article through Medium's import flow, checks the editor against the source Markdown, and can repair a small set of common formatting problems.
TL;DR
Checkout the stable v0.2.3 release, build the @paladini/medium-publisher-mcp package, log in once, and create a Medium draft with publish-devto. Keep the default draft behavior while you inspect the title, code blocks, headings, lists, and metadata.
Prerequisites
You need:
- Node.js 20 or newer.
- A published DEV.to article with a public URL.
- A Medium account that can create stories.
- A terminal that can run npm and the browser installation step.
The package uses Patchright browser automation and a saved browser session. It does not use a Medium write API key. The project documents Medium UI changes as a compatibility risk, so treat the browser session and the resulting draft as reviewable state rather than an unattended guarantee.
Install the released source
The repository's v0.2.3 release is the stable reference for this walkthrough. Installing from that tag keeps the commands separate from later changes on the default branch.
git clone https://github.com/paladini/publish-agents.git
cd publish-agents
git checkout v0.2.3
npm install
npm run build -w @paladini/medium-publisher-mcp
npm link -w @paladini/medium-publisher-mcp
The build produces the CLI and MCP server from the package source. The package declares Node.js 20 or newer and uses patchright as its browser automation dependency. Its post-install step may install the bundled Chromium browser. If that step was skipped in your environment, run the browser installation command documented by Patchright before continuing.
Create a saved Medium session
Log in interactively once:
medium-publisher login
Then check the session in JSON form:
medium-publisher session-check --json
The project stores browser state in an operating-system data directory by default. On Windows, the documented default is %LOCALAPPDATA%\medium-publisher\storageState.json. You can override it with MEDIUM_STATE_PATH or MEDIUM_PUBLISHER_HOME.
This is an important boundary: the saved state contains authentication cookies. Protect the directory like any other local credential store, do not commit it, and do not copy it into a CI artifact.
Import a DEV.to article as a draft
Use the one-shot command with --draft and --json:
medium-publisher publish-devto `
--url "https://dev.to/your-name/your-tutorial" `
--draft --json
The URL must point to a public article. The package fetches the DEV.to article through its public API, then opens Medium's official import route. The --draft flag matters because it leaves the final publication decision with you.
The JSON result includes a Medium URL and metadata details such as whether the title was set, which topics were selected, and whether a hero image was detected. Treat that URL as a draft inspection target until you have checked the actual editor.
Verify the imported editor
Open the returned draft URL in the browser, or use the package's structured extraction command:
medium-publisher extract `
--url "https://medium.com/p/your-draft/edit" `
--json
The extraction workflow is useful because it reports the editor outline and formatting flags. Check at least these points manually:
- The title matches the DEV.to source.
- Headings are rendered as headings, not literal Markdown text.
- Each fenced code example is one code block with the expected language.
- Lists, links, and paragraphs remain in the expected order.
- The subtitle and topics describe the article without inventing claims.
- No secrets or private URLs were imported accidentally.
The package includes a source review that compares the editor content with the DEV.to Markdown. It also has a security check for secret-like values and unexpected short links. These checks reduce risk, but they cannot replace reading the draft in the Medium editor.
Repair common formatting failures
If extraction identifies a problem, create an actions file and apply only the targeted fixes. For example:
[
{ "type": "removeEmptyCodeBlocks" },
{ "type": "mergeAdjacentCodeBlocks" },
{ "type": "promoteDemoteHeading", "blockIndex": 4, "level": 2 }
]
Apply it to the draft:
medium-publisher fix-draft `
--url "https://medium.com/p/your-draft/edit" `
--actions-file fixes.json `
--json
Run extract again after the change. A repair is complete only when the editor and the source agree on the affected blocks. If the editor has changed since the release, stop and inspect the draft manually instead of repeatedly applying actions.
What success looks like
A successful draft run gives you a Medium editor URL, the expected title, readable code blocks, intact headings and lists, and metadata that you can explain. The command does not prove that Medium will preserve every future import. It gives you a repeatable starting point and evidence for a human review.
For a final pre-publication check, compare the draft with the original DEV.to article block by block. Pay particular attention to content after the first code block because malformed fences can make later paragraphs appear to be code. The repository's own test suite covers its formatting and metadata helpers, but your article is still the source of truth for the review.
MCP option for agent-assisted workflows
The same package exposes an MCP server. Register medium-publisher-mcp with your MCP client and use medium_publish_from_devto with publish: false for a draft-first flow. The project documents stdio configuration for clients such as Cursor and Claude Code.
An agent can prepare the URL and summarize the extraction result, but the account owner should approve login, external publication, and any formatting correction. Browser automation is an integration boundary, not a reason to remove human review.
Failure modes and limitations
The session is missing or expired
The CLI uses exit code 3 for a missing or expired session. Run medium-publisher login again. A two-factor challenge or bot check may require headed browser interaction.
The import hangs
The source article must be public because the workflow reads its public DEV.to representation. Confirm that the URL loads without authentication and that the article has been published.
Topics or selectors do not match
Medium's interface can change. The project waits for topic autocomplete and targets the current import and editor controls, but a future UI change can still break a selector. Use extract, inspect the draft, and report reproducible failures to the repository rather than publishing blindly.
The content contains sensitive data
Stop the workflow if the source or editor contains credentials, private links, or unexpected content. Delete the draft through Medium's controls as appropriate, rotate exposed credentials, and review the local browser-state directory.
FAQ
Does this need a Medium API key?
No. The documented workflow uses browser automation and a persistent login session.
Can it publish immediately?
The CLI supports publishing options, but this tutorial deliberately uses --draft. Review the imported editor before choosing a live publication action.
Does it import any URL?
The package supports public URL import, while the one-shot publish-devto path expects a public DEV.to article so it can compare the imported result with the source Markdown.
Is this a guarantee that Medium formatting will stay correct?
No. Medium UI changes, account challenges, and article-specific content can still require manual intervention.
AI assistance disclosure
AI assistance was used to organize this tutorial and check its wording. Commands, version references, workflow boundaries, and limitations were checked against the public publish-agents repository, its v0.2.3 release, and the package's documented source behavior.
Takeaway
Cross-posting is safer when import, repair, and publication are separate decisions. Use publish-devto --draft, inspect the real Medium editor, rerun extraction after any fix, and publish only after the source and destination agree.
Have you found a Medium import failure that a block-level source review catches earlier than a visual skim?
Top comments (0)