# pdrive > Markdown document publishing platform that turns your docs into clean, readable URLs with version control, team collaboration, and programmatic access via MCP and JSON API. pdrive lets teams publish, version, and collaborate on markdown documents. Every document gets a permanent URL, full version history, and automatic rendering to HTML. Projects can be public (visible to anyone) or private (restricted to team members). Access is controlled through account and project memberships with role-based permissions (viewer, editor, manager, admin, owner). Built for engineering docs, RFCs, ADRs, runbooks, API references, AI-generated specs, onboarding guides, and any technical writing that benefits from version control and clean URLs. For the full version with all documentation content inlined, see [llms-full.txt](/llms-full.txt). ## API Access - [JSON API Documentation](/docs/api): RESTful API for reading and publishing documents, including API key authentication - [MCP Server](/docs/mcp): Model Context Protocol server for LLM tool integration ## Documentation - [Getting Started](/docs/getting-started): Quick start guide - [Features](/features): Platform capabilities overview - [Security](/security): Security practices and policies - [Changelog](/changelog): Recent updates and changes ## Key Concepts - **Accounts**: Organizations or individuals with a unique namespace (e.g., `acme`) - **Projects**: Collections of documents within an account (e.g., `docs`, `blog`) - **Documents**: Markdown files with full version history, identified by filename - **Versions**: Immutable snapshots created on each upload, with optional tags and messages ## MCP Tools The MCP server at `/mcp` authenticates via OAuth 2.0 (authorization code flow with PKCE). Discovery endpoint: `/.well-known/oauth-authorization-server`. API keys can also be used as Bearer tokens. - `create_project`: Creates a new project in an account. Requires write scope. - `diff_versions`: Compares two versions of a document and returns a line-level diff. Defaults to comparing the previous version with the latest. Each line has a type (eq, ins, del) and the text content. Also returns addition and deletion counts. - `get_account`: Gets detailed information about an account including stats and permissions. - `get_document`: Reads the latest version of a document. Returns resource_uri for use with subscriptions. - `get_project`: Gets detailed information about a project including stats and last activity. - `get_table_of_contents`: Returns the heading outline of a document. Defaults to the latest version; pass a version number to get a specific version's outline. Each entry has slug, text, level (raw HTML heading level), and display_level (normalized indent for TOC rendering). Useful for understanding document structure without reading the full content. - `get_version`: Reads a specific version of a document. Returns both resource_uri and version_resource_uri. - `list_accounts`: Call this first. Returns the authenticated user and their accounts needed for other pdrive operations. - `list_documents`: Lists documents in a project. Each document includes a resource_uri for use with MCP resource subscriptions. - `list_projects`: Lists projects in an account. Each project includes a resource_uri for use with MCP resource subscriptions. - `list_versions`: Lists all versions of a document. Each version includes a version_resource_uri. - `publish_document`: Publishes a document. Creates it if it doesn't exist, updates it if it does. Returns resource_uri for the document. ## JSON API Documentation ## Quick start Create an API key from your account settings, then publish a document: ```bash # Publish a new document (POST creates, fails if it exists) curl -X POST https://pdrive.io/api/acme/docs/readme.md \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"content": "# Hello\n\nThis is my first document."}' # Create or update (PUT creates if new, adds a version if it exists) curl -X PUT https://pdrive.io/api/acme/docs/readme.md \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"content": "# Hello\n\nUpdated content.", "message": "Fix typo"}' ``` ## Authentication All API requests require a Bearer token in the `Authorization` header: ``` Authorization: Bearer YOUR_API_KEY ``` API keys are scoped to an account. You can create and revoke them from **Settings > API keys** in the web UI, or manage them via the API itself (`/api/:account/-/api-keys`). Keys have a scope of `read` or `read write`. Read-only keys cannot make POST, PUT, PATCH, or DELETE requests. ## Versioning and stability The pdrive API does not use URL versioning. The current API surface lives at `/api/...` and the [changelog](/changelog) tells you what's changed. **Stability commitment** Additive changes ship without notice. New fields, new endpoints, and new optional parameters can appear at any time. Write your client to ignore unknown fields rather than fail on them. Breaking changes are announced in the [changelog](/changelog) at least four weeks before they ship. During the deprecation window, affected endpoints return `Deprecation` and `Sunset` response headers pointing to the changelog entry. We will not silently remove a field or change a response shape. **Why no `/v1/` prefix** URL versioning commits to a specific strategy before we know what kind of breaking changes we will need. Most modern SaaS APIs (Stripe, Linear, Plaid, Resend) have moved away from URL versioning toward header-based or no versioning combined with a clear stability policy. We follow that pattern. If we ever need to ship coordinated breaking changes that cannot be handled by per-field deprecation, we will add explicit versioning at that time and announce it in the changelog with a long migration window. **What this means for you** - Write your client against the current API. Pin nothing. - Subscribe to the [changelog](/changelog). Deprecations are announced there. - Check response headers. `Deprecation` and `Sunset` tell you when something is going away. - Be tolerant of additive changes. New fields will appear in responses over time. ## The `.json` convention Append `.json` to any pdrive URL to get its JSON representation: ```bash # HTML version https://pdrive.io/acme/docs/readme.md # JSON version https://pdrive.io/acme/docs/readme.md.json ``` ## URL conventions All API endpoints follow the pattern `/api/:account_namespace/:project_slug/:filename`. The `:account_namespace` is the account's unique namespace (e.g. `acme`), `:project_slug` is the project's slug, and `:filename` is the document filename including extension. Action endpoints use the `/-/` namespace to avoid collision with wildcards (e.g. `/api/acme/-/projects`). ## ID format IDs are prefixed, checksummed strings of 32-35 characters, format `<3-6 char type>_<24-char body><4-char checksum>`. Examples: - `doc_V1StGXR8Z5jdHi6BmyT0xq3k9fK2` for a document - `user_abc123def456ghi789jkl012XyZq` for a user - `audit_9fK2p8QnR4mL3tX6vN7wY8z1aBcD` for an audit event The prefix tells you the resource type at a glance. The 4-char trailing checksum catches transcription errors before a DB round-trip: a malformed ID returns `422 invalid_id` instead of `404 not found`. Treat IDs as opaque strings in your client. The type prefix is stable and can be used for routing or type-guarding, but the body and checksum are implementation details. Date fields use ISO 8601 UTC format. ## Pagination All list endpoints use cursor-based pagination with a standard response envelope: ```json { "items": [...], "next_cursor": "eyJpIj...", "has_more": true } ``` Pass `?cursor=` to fetch the next page and `?limit=N` to control page size (default 50, max 200). When `has_more` is `false`, `next_cursor` is `null` and there are no more results. Treat cursors as opaque strings. Do not parse, store long-term, or construct them manually. This envelope applies to projects, documents, members, API keys, versions, notifications, deleted documents, and audit log results. User search (`/api/users/search`) is the one exception. It returns `{"items": [...]}` with a fixed limit and no cursor. ## Endpoints ### Identity ``` GET /api/me Current user info GET /api/me/accounts Accounts the user belongs to ``` ### Accounts ``` GET /api/:account Account details DELETE /api/:account/-/settings Soft-delete account (owner) POST /api/:account/-/restore Restore account (owner) ``` The account response includes project and member counts and a `urls` map with links to related resources. Plan-gated links (`api_keys`, `audit_log`) only appear when the account's plan enables those features. ### Projects ``` GET /api/:account/:project Project details GET /api/:account/-/projects List projects (?status=archived|all) POST /api/:account/-/projects Create a project (manager+) ``` The project detail response includes document and member counts and a `urls` map. The list endpoint returns the standard `{items, next_cursor, has_more}` envelope. Create a project: ```bash curl -X POST https://pdrive.io/api/acme/-/projects \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "My Project"}' ``` Body fields: `name` (required), `description`, `is_public` (default false), `is_members_only` (default false). ### Project settings ``` GET /api/:account/:project/-/settings View settings (manager+) PATCH /api/:account/:project/-/settings Update settings (manager+) PATCH /api/:account/:project/-/settings/visibility Update visibility (manager+) POST /api/:account/:project/-/archive Archive project (manager+) DELETE /api/:account/:project/-/archive Unarchive project (manager+) DELETE /api/:account/:project/-/settings Soft-delete project (admin+) ``` Update settings body fields: `name`, `slug`, `description` (all optional). Update visibility body fields: `is_public`, `is_members_only` (all optional, booleans). ### Documents ``` GET /api/:account/:project/-/documents List documents GET /api/:account/:project/:filename Read document with content POST /api/:account/:project/:filename Create document (editor+, 409 if exists) PUT /api/:account/:project/:filename Create or update (editor+) PATCH /api/:account/:project/:filename Rename, move, or update content (see below) DELETE /api/:account/:project/:filename Soft-delete document (editor+) ``` The list endpoint returns `{items, next_cursor, has_more}`. The document detail response includes version count and a `urls` map with links to the versions list and diff endpoints. Publish a document: ```bash curl -X PUT https://pdrive.io/api/acme/docs/deploy-guide.md \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"content": "# Deploy guide\n\nStep-by-step instructions.", "message": "Initial version"}' ``` POST and PUT body fields: `content` (required), `title` (optional, used on create only), `message` (optional, version message), `tags` (optional, array of strings). PUT also accepts `force` (optional boolean, bypasses publish conflict detection). **PATCH dispatch:** The PATCH endpoint accepts one of three body shapes: - `{"filename": "new-name.md"}` to rename (editor+) - `{"project_slug": "target-project"}` to move (admin+) - `{"content": "..."}` to update content (editor+, also accepts optional `message` and `tags`) Only one operation per request. Sending more than one of `filename`, `project_slug`, or `content` returns 422. ### Versions ``` GET /api/:account/:project/:filename/versions List versions GET /api/:account/:project/:filename/versions/:n Read specific version DELETE /api/:account/:project/:filename/versions/:n Soft-delete a version (editor+) GET /api/:account/:project/:filename/diff Diff between versions (?from=N&to=M) ``` The versions list returns `{items, next_cursor, has_more}`. ### Document actions ``` POST /api/:account/:project/:filename/lock Lock document (manager+) DELETE /api/:account/:project/:filename/lock Unlock document (manager+) PATCH /api/:account/:project/:filename/versions/:n/tags Update version tags (editor+) ``` Update tags body fields: `tags` (required, array of strings, max 10 tags, max 30 characters each). ### Account membership ``` GET /api/:account/-/members List members (admin+) POST /api/:account/-/members Invite member (admin+) PATCH /api/:account/-/members/:id Update role (admin+) DELETE /api/:account/-/members/:id Remove or withdraw invite (admin+) POST /api/:account/-/members/:id/accept Accept invite POST /api/:account/-/members/:id/decline Decline invite ``` The list endpoint returns `{items, next_cursor, has_more}`. Invite body fields: `username` or `email` (required), `role` (optional, default `viewer`). Valid roles: `viewer`, `editor`, `manager`, `admin`. Update role body fields: `role` (required). ### Project membership ``` GET /api/:account/:project/-/members List members (manager+) POST /api/:account/:project/-/members Invite member (manager+) PATCH /api/:account/:project/-/members/:id Update role (manager+) DELETE /api/:account/:project/-/members/:id Remove or withdraw invite (manager+) POST /api/:account/:project/-/members/:id/accept Accept invite POST /api/:account/:project/-/members/:id/decline Decline invite ``` The list endpoint returns `{items, next_cursor, has_more}`. Invite body fields: `username` or `email` (required), `role` (optional, default `editor`). Valid roles: `viewer`, `editor`, `manager`. Update role body fields: `role` (required). ### API key management ``` GET /api/:account/-/api-keys List API keys (admin+) POST /api/:account/-/api-keys Create API key (admin+) DELETE /api/:account/-/api-keys/:id Revoke API key (admin+) ``` The list endpoint returns `{items, next_cursor, has_more}`. Create body fields: `label` (required), `scope` (optional, `read` or `read write`, default `read`), `description` (optional), `expires_at` (optional, ISO 8601 datetime). ### Audit log ``` GET /api/:account/-/audit-log Query audit events (admin+) GET /api/:account/-/audit-log/export?format=csv Export as CSV (admin+) ``` The query endpoint returns `{items, next_cursor, has_more}` and supports filters: `?category=`, `?resource=`, `?verb=`, `?username=`, `?project_slug=`, `?from=`, `?to=`, `?search=`. The export endpoint supports a subset of filters: `?category=`, `?resource=`, `?verb=`, `?search=`, `?from=`, `?to=`. The `format=csv` parameter is required. ### Soft-delete and restore ``` DELETE /api/:account/:project/:filename Soft-delete document POST /api/:account/:project/:filename/-/restore Restore document (admin+) POST /api/:account/:project/-/bulk-delete Bulk delete documents (editor+) GET /api/:account/:project/-/deleted List deleted documents POST /api/:account/:project/-/restore Restore project (admin+) DELETE /api/:account/-/settings Soft-delete account (owner) POST /api/:account/-/restore Restore account (owner) ``` The deleted documents list returns `{items, next_cursor, has_more}`. Bulk delete body fields: `filenames` (required, array of filename strings). ### Notifications ``` GET /api/notifications List notifications POST /api/notifications/read-all Mark all as read POST /api/notifications/:id/read Mark one as read ``` The list endpoint returns `{items, next_cursor, has_more}`. Supports `?unread_only=true` to filter to unread notifications only. ### User search ``` GET /api/users/search?q= Search users by username or email ``` Returns `{"items": [...]}` with up to 10 results. No cursor pagination. ## Error format Every error response uses the same shape: ```json {"error": "human-readable message", "code": "machine_readable_code"} ``` | Status | When | | ------ | ---- | | 401 | No auth, invalid API key, expired API key | | 403 | Authenticated but lacks permission | | 404 | Account, project, document, or version not found | | 409 | Conflict (filename exists, publish conflict) | | 413 | Size, storage, or count limits exceeded | | 422 | Validation error (missing field, invalid filename, secret detected, malformed or wrong-type ID. Codes: `invalid_id`, `wrong_id_type`) | | 423 | Document is locked | | 429 | Rate limit exceeded | ## Rate limits API requests are rate limited per API key. Write operations (POST, PUT, PATCH, DELETE) have a lower limit than reads. If you exceed the limit, the API returns `429 Too Many Requests` with a `Retry-After` header indicating how many seconds to wait. --- ## MCP Server ## Installation The pdrive MCP server supports OAuth with dynamic client registration. Your editor handles the authentication flow automatically. No API keys needed. ### Claude Code Add to your `.claude/settings.json` or project `.mcp.json`: ```json { "mcpServers": { "pdrive": { "type": "url", "url": "https://pdrive.io/mcp" } } } ``` When you first connect, your editor will open a browser window to authorize access to your pdrive account. ### Cursor Add to your Cursor MCP settings (`.cursor/mcp.json`): ```json { "mcpServers": { "pdrive": { "type": "url", "url": "https://pdrive.io/mcp" } } } ``` ### Windsurf Add to your Windsurf MCP configuration (`~/.windsurf/mcp.json`): ```json { "mcpServers": { "pdrive": { "type": "url", "url": "https://pdrive.io/mcp" } } } ``` ### API key authentication If your editor does not support OAuth, you can authenticate with an API key instead. Create one at **Settings > API keys** and pass it as a Bearer token: ```json { "mcpServers": { "pdrive": { "type": "url", "url": "https://pdrive.io/mcp", "headers": { "Authorization": "Bearer YOUR_API_KEY" } } } } ``` ## Tools The MCP server exposes these tools: ### `list_accounts` List all accounts you have access to. ### `list_projects` List projects in an account. Requires `account`. ### `list_documents` List documents in a project. Requires `account` and `project`. ### `get_document` Get a document's metadata and latest version content. Requires `account`, `project`, and `filename`. ### `create_project` Create a new project in an account. Requires `account` and `name`. Needs write scope. ### `publish_document` Publish a markdown document to a project. Requires `account`, `project`, `filename`, and `content`. If a document with that filename already exists, a new version is created. Needs write scope. Example tool call: ```json { "tool": "publish_document", "arguments": { "account": "acme", "project": "docs", "filename": "architecture.md", "content": "# Architecture\n\nOverview of the system design." } } ``` ## Resources The MCP server exposes document content as resources. Each document is available at: ``` pdrive://documents/{document_id} ``` Your editor can read these resources to access document content directly. ## Prompts The MCP server includes prompts for common workflows: - **publish_document**: guides you through publishing a new document or updating an existing one - **review_document**: fetches a document and helps review its content --- ## Getting Started ## Create your account Sign up at [pdrive.io](https://pdrive.io) using GitHub OAuth. Once logged in, you land on your account dashboard. If you were invited to an existing account, you'll see it listed after login. Otherwise, create a new account from the dashboard. ## Create a project Click **Create project** from your account dashboard. Give it a name and an optional description. Projects group related documents together. Each project has its own members, roles, and published URL namespace. ## Publish your first document From your project page, click **Upload** and choose a `.md` file from your computer. You can also drag and drop files directly onto the project page. pdrive renders your markdown with syntax highlighting, mermaid diagrams, and heading anchors. Each upload creates a new version, so you can always go back to a previous one. ## What's next - [Set up the JSON API](/docs/api) to publish from scripts and CI - [Connect the MCP server](/docs/mcp) to publish from Claude Code, Cursor, or Windsurf - [Learn all the ways to upload](/docs/uploading) documents