Outpost
by andrezani
README.md
# Outpost
> **Social media API and MCP server for AI agents. Publish to X, Instagram, LinkedIn, Reddit, Bluesky, and Threads from a single endpoint.**
Post to X, LinkedIn, Instagram, Reddit, Bluesky, and Threads — from any AI agent — with a single API call. Outpost understands agent needs: structured errors with `agentHint`, MCP native integration for Claude Desktop/Cursor, and per-org credential isolation (the thing Postiz literally cannot do).
<!-- tags: social-media, mcp, multi-platform, agents, publish, ai-agents, oauth, webhooks -->
---
## One-Liner Quickstart
```bash
# Self-host with Docker (Postgres + Redis included):
git clone https://github.com/andrezani/outpost Outpost && cd Outpost && cp .env.example .env && docker compose up -d && docker compose exec app npm run seed:admin
```
Then post to any platform:
```bash
curl -X POST http://localhost:3000/api/v1/publish \
-H "X-API-Key: sa_xxx" \
-H "Content-Type: application/json" \
-d '{"platform":"x","accountId":"<id>","content":{"text":"Hello from Outpost!"}}'
```
---
## Why Outpost
- **Agent-native errors** — every failure returns `code` + `agentHint` so your LLM knows exactly what to do
- **MCP server included** — works natively in Claude Desktop, Cursor, and any MCP-compatible agent
- **Multi-tenant OAuth** — each org connects their own social accounts (not shared credentials like Postiz)
- **One unified endpoint** — `POST /api/v1/publish` posts to any platform, same request shape
- **6 platforms, all working** — X (Twitter), LinkedIn, Instagram, Reddit, Bluesky, Threads
---
## Stack
| Layer | Technology |
|-------|-----------|
| Framework | NestJS 11 (TypeScript) |
| Database | PostgreSQL 16 via Prisma |
| Cache | Redis 7 |
| Auth | API Key (`X-API-Key` or `Authorization: Bearer`) |
| MCP | stdio transport (Claude Desktop, Cursor) |
---
## Monorepo map
This repository is now the canonical Outpost monorepo:
- `./` — NestJS API, Prisma schema, Docker infra, and core product code
- `packages/mcp/` — canonical in-repo MCP package
- `packages/outpost-mcp-standalone/` — legacy standalone MCP repo imported for history/reference
- `apps/landing/` — static marketing site imported from the old `outpost-landing` repo
The old standalone GitHub repos are legacy sources only. New agent work should happen in this repo on `dev`.
---
## Quick Start
### Prerequisites
- Node.js 18+
- Docker (for Postgres + Redis)
### 1. Install
```bash
git clone https://github.com/andrezani/outpost Outpost
cd Outpost
npm install
npx prisma generate
```
### 2. Configure environment
```bash
cp .env.example .env
# Edit .env — DATABASE_URL and social platform credentials
```
### 3. Start infrastructure
```bash
docker compose up -d
```
### 4. Run migrations + seed
```bash
npx prisma migrate dev
```
### 5. Start
```bash
npm run start:dev
```
---
## API Reference
### Authentication
All endpoints (except `POST /organizations` and `GET /health`) require an API key:
```
X-API-Key: sa_xxx
# or
Authorization: Bearer sa_xxx
```
Create your organization + get an API key:
```bash
curl -X POST http://localhost:3000/organizations \
-H "Content-Type: application/json" \
-d '{"name": "My Org"}'
```
---
### POST /api/v1/publish
Publish a post to any platform.
```bash
curl -X POST http://localhost:3000/api/v1/publish \
-H "X-API-Key: sa_xxx" \
-H "Content-Type: application/json" \
-d '{
"platform": "x",
"accountId": "clxxx",
"content": {
"text": "Hello from Outpost! 🤖"
}
}'
```
**Success response:**
```json
{
"success": true,
"postId": "1234567890",
"platform": "x",
"url": "https://x.com/user/status/1234567890",
"publishedAt": "2026-03-25T03:35:00.000Z"
}
```
**Failure response (agent-parseable):**
```json
{
"success": false,
"error": {
"code": "RATE_LIMITED",
"message": "X rate limit exceeded",
"agentHint": "Retry after 2026-03-25T03:45:00Z. Rate limit resets every 15 minutes.",
"retryAfter": "2026-03-25T03:45:00Z"
}
}
```
**Error codes:**
| Code | Meaning |
|------|---------|
| `AUTH_EXPIRED` | Token expired — reconnect account |
| `AUTH_INVALID` | Token revoked — full re-auth needed |
| `RATE_LIMITED` | Platform rate limit hit |
| `ORG_QUOTA_EXCEEDED` | Monthly free tier (100 posts) exhausted |
| `CONTENT_TOO_LONG` | Text exceeds platform limit |
| `CONTENT_POLICY` | Platform rejected for policy violation |
| `MEDIA_TOO_LARGE` | Media file too large |
| `MEDIA_TYPE_UNSUPPORTED` | Platform doesn't support this media type |
| `ACCOUNT_NOT_FOUND` | accountId not found in org |
| `PLATFORM_ERROR` | Generic platform-side error |
| `PLATFORM_DOWN` | Platform API unavailable |
| `SUBREDDIT_REQUIRED` | Reddit post missing subreddit |
| `SUBREDDIT_NOT_FOUND` | Subreddit doesn't exist |
---
### GET /api/v1/platforms
List all platforms + capabilities. Call this before composing content.
```bash
curl http://localhost:3000/api/v1/platforms -H "X-API-Key: sa_xxx"
```
### GET /api/v1/platforms/:platform/capabilities
Check a specific platform's text limits, media types, rate limits.
```bash
curl http://localhost:3000/api/v1/platforms/x/capabilities -H "X-API-Key: sa_xxx"
```
---
### Account Management
```bash
# List connected accounts
GET /api/v1/accounts
# Connect X via OAuth
POST /api/v1/accounts/connect/x
Body: { "redirectUri": "https://yourapp.com/callback" }
→ Returns: { "authUrl": "...", "state": "...", "instructions": "..." }
# OAuth callback
POST /api/v1/accounts/connect/x/callback
Body: { "code": "...", "state": "..." }
# Connect Bluesky (app password)
POST /api/v1/accounts/connect/bluesky
Body: { "handle": "user.bsky.social", "appPassword": "xxxx-xxxx-xxxx-xxxx" }
# Disconnect account
DELETE /api/v1/accounts/:id
# Check rate limits
GET /api/v1/accounts/:id/rate-limits
```
---
### Webhooks
Get notified when posts succeed or fail:
```bash
# Register webhook
POST /api/v1/webhooks
Body: { "url": "https://your-agent.com/hook", "events": ["post_published", "post_failed"] }
```
Payload:
```json
{
"event": "post.published",
"postId": "1234567890",
"platform": "x",
"url": "https://x.com/user/status/1234567890",
"timestamp": "2026-03-25T03:35:00.000Z"
}
```
Delivery: 3 retries with exponential backoff (1s → 5s → 30s). HMAC-SHA256 signed.
---
## MCP Server (Claude Desktop / Cursor)
Outpost ships a built-in MCP server — use it with Claude Desktop, Cursor, or any MCP-compatible agent.
**Step 1:** Build the MCP server
```bash
npm run build
```
**Step 2:** Add to your MCP config:
**Claude Desktop** (`~/.claude_desktop_config.json`):
```json
{
"mcpServers": {
"outpost": {
"command": "node",
"args": ["/path/to/Outpost/dist/mcp/mcp-server.js"],
"env": {
"OUTPOST_API_KEY": "sa_xxx",
"OUTPOST_BASE_URL": "http://localhost:3000"
}
}
}
}
```
**Cursor** (`.cursor/mcp.json` in your project or `~/.cursor/mcp.json` globally):
```json
{
"mcpServers": {
"outpost": {
"command": "node",
"args": ["/path/to/Outpost/dist/mcp/mcp-server.js"],
"env": {
"OUTPOST_API_KEY": "sa_xxx",
"OUTPOST_BASE_URL": "http://localhost:3000"
}
}
}
}
```
Once published to npm, use `npx` instead:
```json
{
"mcpServers": {
"outpost": {
"command": "npx",
"args": ["-y", "@outpost/mcp-server"],
"env": { "OUTPOST_API_KEY": "sa_xxx" }
}
}
}
```
Build first: `npm run build`
**MCP Tools available:**
| Tool | Description |
|------|-------------|
| `publish_post` | Publish to any platform |
| `list_accounts` | List connected accounts |
| `check_platform_capabilities` | Get text limits, media types, rate limits |
| `check_rate_limits` | Current rate limit status for an account |
| `list_all_platform_capabilities` | All platforms at once |
| `get_post_status` | Check status of a published post |
---
## Platform OAuth Setup
Each platform requires its own developer app. Add credentials to `.env`.
| Platform | Auth method | Setup URL |
|----------|------------|-----------|
| X (Twitter) | OAuth 2.0 PKCE | [developer.twitter.com](https://developer.twitter.com/en/apps) |
| Reddit | OAuth 2.0 | [reddit.com/prefs/apps](https://www.reddit.com/prefs/apps) |
| Instagram | Meta OAuth | [developers.facebook.com](https://developers.facebook.com/apps/) |
| LinkedIn | OAuth 2.0 | [linkedin.com/developers](https://www.linkedin.com/developers/apps) |
| Threads | Meta OAuth | Same Meta app as Instagram |
| Bluesky | App password | No setup needed — user provides app password |
> ⚠️ **Instagram/Threads:** Use the "Other/legacy" Meta app type, NOT the new use-case wizard (it auto-attaches deprecated scopes that get rejected).
> ⚠️ **X OAuth:** Add your callback URL to the Twitter developer app or OAuth will silently fail.
---
## Self-Host with Docker
Deploy the full stack (app + postgres + redis) in 5 commands:
```bash
# 1. Clone the repo
git clone https://github.com/andrezani/outpost Outpost && cd Outpost
# 2. Copy and fill in credentials
cp .env.example .env
# Edit .env — add your social platform client IDs/secrets
# 3. Start everything
docker compose up -d
# 4. Seed your admin org + get API key
docker compose exec app npm run seed:admin
# 5. Open Swagger playground
open http://localhost:3000/api
```
> The app container runs `prisma migrate deploy` automatically on startup.
> Postgres and Redis data persist in named Docker volumes across restarts.
---
## Development
```bash
# Run in watch mode
npm run start:dev
# TypeScript check (must be 0 errors before committing)
node node_modules/typescript/bin/tsc --noEmit -p tsconfig.json
# Regenerate Prisma client after schema changes
npx prisma generate
# Run tests
npm test
```
Pre-commit hook runs `prisma generate` + `tsc --noEmit` automatically.
---
## License
MIT — see [LICENSE](./LICENSE).
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues