Skip to main content
Glama
theworker02

PostPilot

by theworker02
README.md
<div align="center">
  <img src="assets/logo.png" alt="PostPilot logo" width="190" />
  <h1>PostPilot</h1>
  <p><strong>Secure, approval-first publishing from ChatGPT and Codex to X.</strong></p>
  <p><a href="https://theworker02.github.io/postpilot/"><strong>Project website</strong></a> · <a href="docs/UPLOAD_CHECKLIST.md">Submission status</a></p>
  <p>
    <a href="https://github.com/theworker02/postpilot/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/theworker02/postpilot/actions/workflows/ci.yml/badge.svg"></a>
    <a href="https://github.com/theworker02/postpilot/actions/workflows/codeql.yml"><img alt="CodeQL" src="https://github.com/theworker02/postpilot/actions/workflows/codeql.yml/badge.svg"></a>
    <a href="https://developers.openai.com/plugins/build/plugins"><img alt="ChatGPT plugin ready" src="https://img.shields.io/badge/ChatGPT-plugin%20ready-10A37F?logo=openai&logoColor=white"></a>
    <a href="LICENSE"><img alt="MIT License" src="https://img.shields.io/badge/license-MIT-12cff3.svg"></a>
    <a href="https://github.com/theworker02/postpilot/releases"><img alt="Release" src="https://img.shields.io/github/v/release/theworker02/postpilot?include_prereleases"></a>
    <a href="https://github.com/sponsors/theworker02"><img alt="GitHub Sponsors" src="https://img.shields.io/github/sponsors/theworker02?logo=githubsponsors&label=Sponsor"></a>
    <a href="https://thanks.dev/u/gh/theworker02"><img alt="thanks.dev" src="https://img.shields.io/badge/thanks.dev-support%20PostPilot-8A2BE2"></a>
  </p>
</div>

PostPilot is an open-source ChatGPT plugin that connects a user's X account and lets ChatGPT draft, validate, review, publish, list, and delete posts through a remote MCP server.

> **Safety first:** PostPilot never publishes from a draft alone. `prepare_post` binds the exact text to a short-lived, single-use approval token. `publish_post` rejects changed text, expired approvals, reused approvals, and approvals belonging to another account.

## What it includes

- Streamable HTTP MCP endpoint at `/mcp`
- X OAuth 2.0 Authorization Code + PKCE
- ChatGPT-compatible OAuth discovery and dynamic client registration
- Per-tool OAuth security declarations and account-linking challenges
- Resource-bound access tokens with issuer, audience, expiry, and scope verification
- Least-privilege X scopes: `tweet.read tweet.write users.read offline.access`
- Weighted X character validation using `twitter-text`
- Exact-text, expiring, single-use publishing approvals
- Tools for account lookup, validation, staging, publishing, recent posts, and deletion
- Helmet, strict request limits, secret redaction, timeouts, typed validation, health checks
- Tests, linting, type checking, CI, Dependabot, CodeQL, Docker, and deployment template
- Security, privacy, contribution, architecture, setup, and submission documentation
- A current `.codex-plugin/plugin.json` manifest with branded install metadata
- A domain-verification endpoint, release notes, and reviewer-ready evaluation cases

## Why PostPilot

Giving a model access to a public social account demands a stronger boundary than “call the post API.” PostPilot deliberately separates writing from publishing:

1. ChatGPT drafts or validates content.
2. `prepare_post` creates a signed approval capability bound to the exact text, account, and optional reply target.
3. The user reviews the exact staged text.
4. Only explicit approval allows `publish_post`.
5. The capability expires after ten minutes and cannot be reused.

Changing even one character invalidates the approval.

## Architecture

```mermaid
flowchart TD
  U["User in ChatGPT"] --> C["ChatGPT MCP client"]
  C -->|"OAuth + tool calls"| P["PostPilot MCP server"]
  P -->|"PKCE authorization"| X["X API"]
  P --> A["Exact-text approval gate"]
  A -->|"Confirmed publish"| X
```

PostPilot is the authorization server from ChatGPT's perspective and an OAuth client of X. This broker pattern lets ChatGPT receive a PostPilot-scoped bearer token without exposing X tokens to the model or widget.

## MCP tools

| Tool | Effect | Confirmation behavior |
|---|---|---|
| `validate_post` | Local validation | No account or confirmation |
| `get_x_account` | Reads linked identity | OAuth required |
| `prepare_post` | Stages exact content | Does not publish |
| `publish_post` | Creates a post | Exact, single-use approval required |
| `recent_posts` | Reads recent posts | OAuth required |
| `delete_post` | Deletes a post | Explicit `confirm: true`; destructive annotation |

Every tool advertises explicit `readOnlyHint`, `destructiveHint`, and `openWorldHint` values. Shared server instructions reinforce the required prepare → review → publish sequence.

## Example conversations

> “Check whether this announcement fits on X.”

PostPilot calls `validate_post`, calculates X's weighted character length, and reports the remaining space without publishing.

> “Prepare ‘PostPilot is now live’ for my review.”

PostPilot stages the exact text and asks for confirmation. It does not publish at this stage.

> “I approve that exact post. Publish it.”

PostPilot verifies the account, signature, expiry, content digest, reply target, and unused token ID before calling X.

## Local setup

1. Create an app in the [X Developer Console](https://developer.x.com/) and enable OAuth 2.0.
2. Add `http://localhost:8787/oauth/x/callback` as a callback URL for local development.
3. Copy `.env.example` to `.env`, fill in the X client ID, optional confidential-client secret, and a random signing secret.
4. Install and run:

```bash
npm install
npm run dev
```

5. Expose port 8787 with a temporary HTTPS tunnel for development only.
6. In ChatGPT, enable Developer mode, create a draft plugin, and use `https://YOUR_HOST/mcp`.

For production, read [deployment](docs/DEPLOYMENT.md), [X setup](docs/X_SETUP.md), and [ChatGPT setup](docs/CHATGPT_SETUP.md).

## Connect from ChatGPT

After deployment, verify these public URLs before adding the server:

| URL | Expected result |
|---|---|
| `https://YOUR_HOST/healthz` | JSON with `ok: true` and version `0.3.0` |
| `https://YOUR_HOST/.well-known/oauth-protected-resource` | MCP resource and authorization-server metadata |
| `https://YOUR_HOST/.well-known/oauth-authorization-server` | OAuth endpoints, PKCE, and dynamic registration metadata |
| `https://YOUR_HOST/mcp` | Streamable HTTP MCP endpoint |

Then open **ChatGPT → Settings → Security and login**, enable Developer mode, open **ChatGPT Plugins**, select **+**, and enter `https://YOUR_HOST/mcp`. `validate_post` works without authentication; the account-specific tools trigger ChatGPT's OAuth linking interface and then redirect the user through X authorization.

## ChatGPT and Codex plugin packaging

PostPilot includes the required manifest at [`.codex-plugin/plugin.json`](.codex-plugin/plugin.json), branded assets, starter prompts, capability metadata, legal links, and submission evaluation cases.

The repository cannot contain a production `.app.json` ID before the MCP connection is registered because ChatGPT generates that `plugin_asdk_app...` identifier. To complete local installation:

1. Deploy the MCP server or expose it temporarily over HTTPS.
2. Register the `/mcp` URL in ChatGPT Developer mode.
3. Copy the generated `plugin_asdk_app...` technical ID.
4. Create `.app.json` using that registered ID and add `"apps": "./.app.json"` to the manifest.
5. Refresh plugin metadata and test in a new conversation.

For a public directory submission, use the production HTTPS endpoint and submission portal rather than a temporary tunnel. See [the submission dossier](docs/SUBMISSION.md).

## Configuration

| Variable | Required | Purpose |
|---|---:|---|
| `PORT` | No | HTTP port; defaults to `8787`. |
| `PUBLIC_BASE_URL` | Production | Canonical HTTPS origin used in OAuth metadata. |
| `X_CLIENT_ID` | Yes | X OAuth 2.0 application client ID. |
| `X_CLIENT_SECRET` | Confidential clients | X OAuth client secret. |
| `X_REDIRECT_URI` | Yes | Exact X callback ending in `/oauth/x/callback`. |
| `APP_SIGNING_SECRET` | Yes | 32+ character secret for access and approval tokens. |
| `ALLOWED_ORIGINS` | Production | Comma-separated CORS origins. |
| `OPENAI_APPS_CHALLENGE` | Submission | Exact domain-verification token from the plugin portal. |
| `LOG_LEVEL` | No | Pino log level. |

## Security properties

- OAuth 2.0 authorization code flow with PKCE on both trust boundaries.
- One-time authorization codes bound to client, redirect URI, challenge, and expiry.
- Exact-text approval capabilities signed with HS256.
- Single-use approval identifiers and ten-minute expiry.
- Server-side validation and authorization for protected tools.
- Authorization headers, OAuth codes, and refresh tokens redacted from logs.
- Outbound X requests use TLS, timeouts, and bounded error bodies.
- Publishing and deletion are explicitly marked destructive and open-world.

Read [SECURITY.md](SECURITY.md) before deploying or reporting a vulnerability.

## Required production work

The default `MemoryStore` is intentionally suitable only for one-process development. Before public deployment, implement the `Store` interface with encrypted, durable storage and atomic single-use approval consumption. Refresh-token rotation should be added to the X client for long-lived connections. See [architecture](docs/ARCHITECTURE.md).

This limitation is why v0.3.0 is a developer release, not a claim that the hosted public service is already production-ready.

## Commands

```bash
npm run check   # lint, types, tests
npm run validate:plugin
npm run build
npm start
```

## Repository layout

```text
.codex-plugin/plugin.json  Plugin package and install metadata
assets/                    Logo and composer icon
docs/                      Architecture, deployment, legal, and submission guides
evals/                     Positive and negative submission cases
src/index.ts               HTTP transport and server bootstrap
src/mcp.ts                 Tools, schemas, instructions, and annotations
src/oauth.ts               ChatGPT ↔ PostPilot ↔ X OAuth broker
src/security.ts            PKCE, JWT access, and approval binding
src/store.ts               Replaceable persistence interface
src/x-client.ts            Minimal typed X API client
```

## Release policy

PostPilot uses semantic versioning. See [CHANGELOG.md](CHANGELOG.md) for release notes. Tool names and schemas should remain backward compatible within a major version.

## Responsible use

Users remain responsible for content they publish and for compliance with X's rules, developer agreement, applicable law, and OpenAI plugin policies. PostPilot must not be used for spam, platform manipulation, impersonation, harassment, or evading rate limits.

## Status

Version 0.3.0 implements the remote Streamable HTTP MCP, ChatGPT OAuth discovery, per-tool security and output schemas, runtime account-linking challenges, resource-bound bearer tokens, valid square branding assets, and automated package validation required for Developer mode testing. Before public submission, complete the operator-controlled items in [the upload checklist](docs/UPLOAD_CHECKLIST.md).

## Support PostPilot

If PostPilot is useful to you, you can support its maintenance through [GitHub Sponsors](https://github.com/sponsors/theworker02) or [thanks.dev](https://thanks.dev/u/gh/theworker02). The repository's [`.github/FUNDING.yml`](.github/FUNDING.yml) exposes both options through GitHub's Sponsor button.

## License

[MIT](LICENSE)