presskit-mcp
A Python FastMCP server and CLI for publishing to Medium and Substack.
Stability warning: Both integrations rely on unofficial/deprecated access paths. Medium's REST API is frozen (no new tokens issued). Both platforms' internal endpoints are undocumented and may change without notice.
---
Install
pip install -e .
This installs two commands:
presskit— CLI for direct publishing from the terminalpublishing-mcp— MCP server for use with Claude Desktop/Claude Code
---
CLI Usage (presskit)
Publish a markdown file
# Publish to Medium as a draft
presskit publish medium --file docs/drafts/my-article.md
# Publish to Substack as a draft
presskit publish substack --file docs/drafts/my-article.md
# Publish to both platforms at once
presskit publish both --file docs/drafts/my-article.md
# Publish live (not draft)
presskit publish medium --file article.md --status public --tags "python,automation"
# Substack: publish to web only (no subscriber email)
presskit publish substack --file article.md --status public --no-email
The CLI reads YAML frontmatter from the markdown file:
---
title: "My Article Title"
subtitle: "Optional subtitle for Substack"
tags: [python, automation, infrastructure]
---
# My Article Title
Article body here...
If no frontmatter title is present, the first # Heading is used.
List posts
# List your Medium posts
presskit list medium --username alexander.g.moore1
# List Substack posts
presskit list substack --subdomain alexgmoore
# List Substack drafts
presskit drafts substack
CLI options reference
presskit publish <platform> [options]
--file, -f Markdown file to publish (required)
--status, -s draft | public | unlisted (default: draft)
--tags, -t Comma-separated tags, e.g. "python,devops" (Medium)
--subtitle Post subtitle (Substack)
--no-email Publish to web only, skip subscriber email (Substack)
presskit list <platform> [options]
--username, -u Medium username without @ (Medium)
--subdomain, -d Substack subdomain (Substack)
--limit, -n Max results (default: 10)
presskit drafts substack
---
MCP Server Usage
Run the server
# Direct
python server.py
# Or via installed entry point
publishing-mcp
Add to Claude Code (project-level)
Create .claude/mcp.json in your project:
{
"mcpServers": {
"publishing": {
"command": "python3",
"args": ["/absolute/path/to/presskit-mcp/server.py"],
"env": {
"MEDIUM_SESSION_COOKIE": "your_sid_cookie",
"MEDIUM_AUTH_STATE_FILE": "/path/to/medium-auth.json",
"SUBSTACK_EMAIL": "you@example.com",
"SUBSTACK_PASSWORD": "your_substack_password",
"SUBSTACK_PUBLICATION_URL": "https://yourpub.substack.com"
}
}
}
}
Add to Claude Desktop
Edit ~/Library/Application Support/Claude/claude_desktop_config.json (Mac):
{
"mcpServers": {
"publishing": {
"command": "python",
"args": ["/absolute/path/to/presskit-mcp/server.py"],
"env": {
"MEDIUM_SESSION_COOKIE": "your_sid",
"MEDIUM_AUTH_STATE_FILE": "/path/to/medium-auth.json",
"SUBSTACK_EMAIL": "you@example.com",
"SUBSTACK_PASSWORD": "your_password",
"SUBSTACK_PUBLICATION_URL": "https://yourpub.substack.com"
}
}
}
}
Restart Claude Desktop/Code after adding the config.
---
FastMCP Development & Testing
presskit-mcp is built on FastMCP. Here's how to work with it during development.
Inspect tools interactively
The MCP Inspector opens a browser UI where you can call any tool, see inputs/outputs, and debug:
# Using the mcp CLI (installed with mcp[cli])
mcp dev server.py
This starts the server and opens an interactive inspector at http://localhost:5173. You can:
- Browse all 15 registered tools
- Fill in parameters and execute them
- See JSON responses in real-time
- Test error handling
Run the server with stdio transport (default)
# FastMCP defaults to stdio transport (what Claude Desktop/Code expects)
python server.py
Run with SSE transport (for remote/HTTP access)
# Start as an HTTP server on port 8000
mcp run server.py --transport sse --port 8000
Then connect from any MCP client using http://localhost:8000/sse.
Call a tool directly via mcp call
# One-shot tool invocation without starting a persistent server
echo '{"username": "alexander.g.moore1", "limit": 5}' | \
mcp call server.py medium_list_posts
List all registered tools
mcp tools server.py
Environment variables for testing
# Medium (session-based — no integration token needed)
export MEDIUM_SESSION_COOKIE="your_sid_value"
export MEDIUM_AUTH_STATE_FILE="/path/to/medium-auth.json"
# Medium (REST API — only if you have an existing token)
export MEDIUM_INTEGRATION_TOKEN="your_token"
# Substack
export SUBSTACK_EMAIL="you@example.com"
export SUBSTACK_PASSWORD="your_password"
export SUBSTACK_PUBLICATION_URL="https://yourpub.substack.com"
Running tests
python3 -m unittest discover -s tests -v
Tests use sys.modules patching to stub third-party deps — no external services needed.
---
Tools Reference
Medium (6 tools)
| Tool | Auth | Method | |---|---|---| | medium_get_current_user | Integration token | REST API | | medium_get_publications | Integration token | REST API | | medium_create_post | Integration token | REST API | | medium_create_post_session | Session cookie | GraphQL + Delta OT | | medium_list_posts | Session cookie | Unofficial GraphQL | | medium_get_post_stats | Session cookie | Unofficial GraphQL |
Substack (9 tools)
| Tool | Auth | Method | |---|---|---| | substack_get_publication_info | Email/password or cookie | python-substack | | substack_get_all_publications | Email/password or cookie | python-substack | | substack_list_posts | None (public) | Raw HTTP | | substack_get_post | None (public) | Raw HTTP | | substack_search_publications | None (public) | Raw HTTP | | substack_get_subscriber_count | Email/password or cookie | python-substack | | substack_list_drafts | Email/password or cookie | python-substack | | substack_create_draft | Email/password or cookie | python-substack | | substack_publish_post | Email/password or cookie | python-substack |
---
Credentials
Medium session cookie
Medium no longer issues API tokens. The session-based tools use browser cookies:
- Log in to medium.com in a browser
- Extract the
sidcookie from DevTools → Application → Cookies - For full functionality, save the complete browser state with
playwright-cli state-save medium-auth.jsonand setMEDIUM_AUTH_STATE_FILE
Substack
Use your Substack email and password directly. If your account uses magic links only:
- Sign out of Substack
- Click "Sign in with password"
- Click "Set a new password"
---
Architecture
presskit-mcp/
├── server.py # FastMCP server entry point (15 tools)
├── cli.py # CLI entry point (presskit command)
├── medium/
│ ├── client.py # REST API + GraphQL + Delta OT HTTP layer
│ └── tools.py # 6 MCP tools with Pydantic input models
├── substack/
│ ├── client.py # python-substack wrapper + raw HTTP
│ └── tools.py # 9 MCP tools with Pydantic input models
├── tests/
│ └── test_static.py # Unit tests (no external deps needed)
├── pyproject.toml # pip installable, entry points
└── config.yaml # Credential reference (not loaded by code)
How Medium session publishing works
Medium's web editor saves content via a delta-based Operational Transform system — not GraphQL or REST. The medium_create_post_session tool replicates this:
- GraphQL
createPost→ creates an empty draft, returnspost_id POST /p/{post_id}/deltas→ writes title + paragraphs as OT deltas- GraphQL
setPostTags→ sets up to 5 tags - GraphQL
publishPost→ publishes (optional)
This was discovered by decompiling Medium's Android APK and capturing network traffic from the web editor.
---
Known limitations
- Medium session cookies expire — you'll need to refresh
medium-auth.jsonperiodically (re-login via browser) - Medium REST API — No new integration tokens. Only existing holders can use
medium_create_post - Medium inline formatting — Bold/italic markup positions in the delta API need accurate character offsets; the current markdown parser sends plain text paragraphs
- Substack — No official API. All endpoints are reverse-engineered. Keep requests under 1/sec
- Images — Neither platform supports programmatic inline image upload via these tools yet












