Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ Audio sites carry a mic badge across the dashboard, and `postplan comments` pref

The hosted app also ships a public `/scratch/` workspace with three no-login sample sites, so visitors can inspect real output before installing anything.

## Cloud MCP

Connect your assistant to `https://<your-postplan-host>/api/mcp`, then sign in and approve access. Claude web/Desktop supports this as a custom connector; Claude Code, Cursor, and Codex can use the same URL. The server runs in Postplan's existing Cloudflare Worker and uses its sessions KV; no extra Worker, database migration, secret, or running laptop is required.

Cloud tools publish file contents, read source and comments, manage feedback, and restore versions. Upload requests are capped at 4 MB and 200 files. Revoke access from **MCP connections** in the account menu. [Setup and tool details](packages/cli/internal/cli/SKILL.md#cloud-mcp).

## CLI

```bash
Expand Down Expand Up @@ -94,7 +100,7 @@ The bundled skill teaches your agent to drive Postplan. Install it into **any**
npx skills add notsuhas/postplan # installs the postplan-cli skill universally (Codex, Cursor, OpenCode, Claude Code …)
```

The `curl … /api/install | sh` line already installs the skill into detected Claude Code, Codex, Cursor, OpenCode, and shared Agent Skills directories. The skill only wraps the `postplan` CLI, so any shell-capable agent works with or without it.
The `curl … /api/install | sh` line already installs the skill into detected Claude Code, Codex, Cursor, OpenCode, and shared Agent Skills directories. The skill covers the CLI and cloud MCP; shell-capable agents can also use the CLI directly.

| command | what it does |
|---|---|
Expand Down
186 changes: 184 additions & 2 deletions bun.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ For any other update, first run `postplan read <space/site> --pull <dir>` and de
- Atomically claim reviewer-sent feedback batches, link deployments to them, and complete them.
- Manage explicit viewer/editor shares with `postplan shares`.
- Fork a site before changing someone else's work.
- No shell? Connect to {{POSTPLAN_ORIGIN}}/api/mcp over Streamable HTTP and sign in with OAuth. Cloud tools accept file contents, not local paths; requests are capped at 4 MB and 200 files. Use stable idempotency keys for deploy, rollback, and feedback claims/completion. Manage access at {{POSTPLAN_ORIGIN}}/api/oauth/connections. `postplan mcp` is an optional local stdio adapter for filesystem uploads.
- Compare deployment versions and restore an older snapshot without deleting history.
- Use `postplan.db` for opt-in browser data and realtime updates.
- Publish animated HTML (a HyperFrames-style GSAP timeline on `window.__timelines`) and it plays with a scrubbable player bar; comments get seekable `[m:ss]` timestamps.
Expand Down
5 changes: 4 additions & 1 deletion packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@
"deploy:content": "bun run build:mermaid && bun run build:chart && bun run build:annotate && bun run build:db && wrangler deploy --config wrangler.content.jsonc"
},
"dependencies": {
"@cloudflare/workers-oauth-provider": "1.2.3",
"@modelcontextprotocol/sdk": "1.32.1",
"@observablehq/plot": "0.6.17",
"@workos-inc/node": "^10.13.0",
"drizzle-orm": "^0.45.2",
"hono": "^4.13.5",
"marked": "^18.0.11",
"mermaid": "12.0.0",
"workers-og": "^0.0.27"
"workers-og": "^0.0.27",
"zod": "4.4.3"
},
"devDependencies": {
"drizzle-kit": "^0.31.10",
Expand Down
6 changes: 4 additions & 2 deletions packages/api/src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { describe, expect, test } from 'bun:test'
import worker from '../index'
import { describe, expect, test, mock } from 'bun:test'
import { STATS_CACHE_KEY, STATS_TOTALS_CACHE_KEY } from '../lib/stats'
import { makeKv } from '../test/harness'

mock.module('cloudflare:workers', () => ({ WorkerEntrypoint: class {} }))
const { default: worker } = await import('../index')

// Composition-level checks against the real worker export (routes registered before the /api/*
// guards need no DB, so a minimal env suffices). Exercised through `worker.fetch` — the handler
// production actually invokes — not a Hono test helper.
Expand Down
4 changes: 2 additions & 2 deletions packages/api/src/agent-docs.ts

Large diffs are not rendered by default.

32 changes: 20 additions & 12 deletions packages/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import { slackEvents } from './routes/slack-events'
import { spaces } from './routes/spaces'
import { stars } from './routes/stars'
import { upload } from './routes/upload'
import { mcpAuth } from './routes/mcp-auth'
import { fetchWithCloudMcp } from './mcp/worker'
import { users } from './routes/users'
import type { AppEnv, Bindings } from './types'

Expand Down Expand Up @@ -80,6 +82,7 @@ app.get('/.well-known/agents.json', (c) => {
homepage: origin,
documentation: `${origin}/llms.txt`,
skill: `${origin}/skills/postplan-cli/SKILL.md`,
mcp: { url: `${origin}/api/mcp`, transport: 'streamable-http' },
health: `${origin}/api/health`,
capabilities: [
'deploy',
Expand Down Expand Up @@ -143,6 +146,7 @@ app.get('/api/config', async (c) => {
})
})
app.route('/api/auth', auth)
app.route('/api/oauth', mcpAuth)
app.route('/api/spaces', spaces)
app.route('/api/sites', sites)
app.route('/api', feedback)
Expand Down Expand Up @@ -196,18 +200,7 @@ const DAILY_PURGE_CRON = '0 3 * * *'
// lib/retention.ts for the delete shapes and how it preserves stats.ts's all-time totals.
export default {
async fetch(request, env, executionCtx) {
const shareShell = await siteShareShell(request, env)
if (shareShell) return shareShell
const pathname = new URL(request.url).pathname
if (
pathname.startsWith('/api/') ||
pathname === '/llms.txt' ||
pathname.startsWith('/skills/') ||
pathname.startsWith('/.well-known/')
) {
return app.fetch(request, env, executionCtx)
}
return env.ASSETS.fetch(request)
return fetchWithCloudMcp(request, env, executionCtx, fetchApp)
},
async scheduled(event, env, _ctx) {
const db = sessionDb(env.POSTPLAN_DB, 'first-unconstrained')
Expand All @@ -218,3 +211,18 @@ export default {
await cachedStats(env.POSTPLAN_SESSIONS, db, (p) => p.then(() => {}))
},
} satisfies ExportedHandler<Bindings>

async function fetchApp(request: Request, env: Bindings, executionCtx: ExecutionContext): Promise<Response> {
const shareShell = await siteShareShell(request, env)
if (shareShell) return shareShell
const pathname = new URL(request.url).pathname
if (
pathname.startsWith('/api/') ||
pathname === '/llms.txt' ||
pathname.startsWith('/skills/') ||
pathname.startsWith('/.well-known/')
) {
return app.fetch(request, env, executionCtx)
}
return env.ASSETS.fetch(request)
}
1 change: 1 addition & 0 deletions packages/api/src/lib/delegated-user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const DELEGATED_USER = Symbol('postplan.mcp.user')
6 changes: 6 additions & 0 deletions packages/api/src/lib/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getUserById } from '../db/repo'
import type { AppEnv, Credential, SessionUser } from '../types'
import { API_KEY_PREFIX, apiKeyDb, resolveApiKey, touchApiKeyLastUsed } from './api-key'
import { fireAndForget } from './events'
import { DELEGATED_USER } from './delegated-user'

const SESSION_COOKIE = '__Host-postplan_session'
const DEV_SESSION_COOKIE = 'postplan_dev_session'
Expand Down Expand Up @@ -182,6 +183,11 @@ export function bearerToken(c: Context<AppEnv>): string | null {
// must NOT fall through to the KV CLI lookup — an attacker presenting a bad API key must not get
// a second shot at the CLI token store (or vice versa).
export async function readCredential(c: Context<AppEnv>): Promise<Credential | null> {
const delegated = c.env[DELEGATED_USER]
if (delegated) {
if (await isUserAccessRevoked(c.env.POSTPLAN_SESSIONS, delegated.id)) return null
return { kind: 'oauth', user: delegated }
}
const sessionUser = await readSession(c)
if (sessionUser) {
if (await isUserAccessRevoked(c.env.POSTPLAN_SESSIONS, sessionUser.id)) return null
Expand Down
Loading
Loading