Skip to main content
Glama
nsmith
by nsmith
README.md
# Markpost

An MCP server that takes Markdown content and syndicates it to **Twitter/X**, **Threads**, and a **static blog on S3**. Designed to be used by AI agents like Claude Code — write a post in Markdown, and Markpost handles the formatting and publishing for each platform.

## What it does

- **Formats per platform** — Strips Markdown to plain text for social media, renders full HTML for blog
- **Auto-splits into threads** — Long content is split at `---` separators or sentence boundaries, respecting each platform's character limit (280 for Twitter, 500 for Threads)
- **Previews before publishing** — Dry-run tool shows exactly how content will be formatted and split
- **Syndicates everywhere at once** — Publish to all platforms with a single tool call, or pick specific ones

## Tools

| Tool | Description |
|------|-------------|
| `publish_post` | Format and publish Markdown to one or more platforms |
| `preview_post` | Preview formatting and thread splits without publishing |
| `ping` | Health check |

## Quick start

### 1. Install

```bash
git clone <this-repo> && cd markpost
uv venv && uv pip install -e ".[dev]"
```

### 2. Configure API keys

Copy the example config and fill in your credentials:

```bash
mkdir -p ~/.markpost
cp config.example.toml ~/.markpost/config.toml
```

Edit `~/.markpost/config.toml`:

```toml
[twitter]
consumer_key = "your-consumer-key"
consumer_secret = "your-consumer-secret"
access_token = "your-access-token"
access_token_secret = "your-access-token-secret"

[threads]
access_token = "your-long-lived-access-token"
user_id = "your-threads-user-id"

[blog]
s3_bucket = "my-blog-bucket"
s3_prefix = "posts/"
base_url = "https://blog.example.com"

[blog.aws]
region = "us-east-1"
```

You can also set a custom config path via the `MARKPOST_CONFIG` environment variable.

### 3. Connect to an MCP client

See the sections below for your specific client.

## Running the server

### stdio mode (default)

stdio is the standard transport for local MCP clients. The server reads JSON-RPC messages from stdin and writes responses to stdout.

```bash
uv run python src/markpost/server.py
```

Or using the FastMCP CLI:

```bash
uv run fastmcp run src/markpost/server.py
```

### HTTP mode (remote / SSE)

For remote access or web-based clients, run in HTTP mode:

```bash
uv run fastmcp run src/markpost/server.py --transport http --host 0.0.0.0 --port 9000
```

The server will be available at `http://localhost:9000`. Clients connect via the Streamable HTTP transport.

## Client setup

### Claude Code

Register the server with the `claude` CLI:

```bash
claude mcp add markpost -- uv run --directory /path/to/markpost fastmcp run src/markpost/server.py
```

Verify it's registered:

```bash
claude mcp list
```

The `publish_post` and `preview_post` tools will now be available in Claude Code sessions.

### Claude Desktop

Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):

```json
{
  "mcpServers": {
    "markpost": {
      "command": "uv",
      "args": [
        "run",
        "--directory", "/path/to/markpost",
        "fastmcp", "run", "src/markpost/server.py"
      ]
    }
  }
}
```

Restart Claude Desktop after saving.

### Other MCP clients (HTTP)

Start the server in HTTP mode (see above), then point your client at `http://localhost:9000`.

## Getting API keys

### Twitter/X

1. Go to the [Twitter Developer Portal](https://developer.twitter.com/)
2. Create a Project and App
3. Set app permissions to **Read and Write**
4. Generate your Consumer Key (API Key), Consumer Secret (API Secret), Access Token, and Access Token Secret
5. If you changed permissions after generating tokens, **regenerate them**

The free tier allows 1,500 tweets/month.

### Threads

1. Go to the [Meta Developer Dashboard](https://developers.facebook.com/)
2. Create a new app and add the **Threads API** product
3. Your Threads account must be **public**
4. Complete the OAuth flow to get an access token:
   - Authorize at `https://threads.net/oauth/authorize?client_id={app_id}&redirect_uri={uri}&scope=threads_basic,threads_content_publish&response_type=code`
   - Exchange the code for a short-lived token at `https://graph.threads.net/oauth/access_token`
   - Exchange for a long-lived token (60 days) at `https://graph.threads.net/access_token?grant_type=th_exchange_token&client_secret={secret}&access_token={short_token}`
5. Get your user ID: `GET https://graph.threads.net/v1.0/me?access_token={token}`

### S3 Blog

1. Create an S3 bucket configured for static website hosting
2. Configure your AWS credentials via the standard methods (`~/.aws/credentials`, environment variables, or IAM role)
3. Set `s3_bucket`, `s3_prefix`, and `base_url` in your config

The `base_url` should be the public URL where your blog is served (e.g., your CloudFront distribution or S3 website endpoint).

## Thread splitting

Long content is automatically split into threads. You control splits two ways:

**Explicit separators** — Use `---` in your Markdown to force a split:

```markdown
Here is the first tweet in my thread.

---

And here is the second one.
```

**Auto-splitting** — If any section exceeds the platform's character limit, it's split at sentence boundaries. If a single sentence is too long, it falls back to word boundaries.

The `preview_post` tool lets you see exactly how content will be split before publishing.

## Development

```bash
# Install dev dependencies
uv pip install -e ".[dev]"

# Run tests
uv run pytest tests/ -v

# Run a specific test file
uv run pytest tests/test_formatter.py -v
```

## Project structure

```
src/markpost/
  server.py              # FastMCP server — ping, publish_post, preview_post
  config.py              # TOML config loading
  formatter.py           # markdown_to_plain, split_into_thread, markdown_to_html
  publishers/
    twitter.py           # Twitter/X via tweepy
    threads.py           # Threads via httpx (async)
    blog.py              # S3 upload via boto3
```

## License

Apache 2.0

TDQS

A3.8/5.0

Scored across 3 tools

Disambiguation5/5

publish_post and preview_post are cleanly separated as actual publishing vs. dry-run preview, and ping is an obvious health check. No two tools overlap in purpose.

Naming Consistency4/5

publish_post and preview_post follow a consistent verb_noun pattern, but ping breaks that pattern. The naming is still readable and predictable overall.

Tool Count5/5

Three tools is a tight, well-scoped set: health check, preview, and publish. Each tool earns its place with no redundancy.

Completeness4/5

The preview-then-publish workflow covers the server's stated purpose well. Minor gaps like listing platforms or managing existing posts exist, but they fall outside the core publish/preview flow.

Maintenance

ActivityInactive
ResponsivenessNo issues