The Campbooks MCP server lives at POST /api/mcp on any Campbooks instance. It implements MCP protocol version 2025-03-26 over streamable HTTP and exposes 72 tools (plus 3 context tools that need no scope).
Authorization
POST /api/mcp accepts three Authorization forms:
| Form | Header value | Notes |
|---|
| MCP key | Bearer <uid>.<client-secret> | Client's own UID and plaintext secret joined by a dot. Doorkeeper UIDs and secrets are dot-free, so the dot is unambiguous. |
| HTTP Basic | Basic base64(uid:secret) | Standard HTTP equivalent of the MCP key form. |
| Doorkeeper token | Bearer <access-token> | Short-lived (2 h) token from POST /api/oauth/token. Works for one-off scripts; not suitable for static agent configs. |
The REST API (/api/v1/*) accepts only Doorkeeper bearer tokens. Forms 1 and 2 — MCP keys — are supported at /api/mcp only.
Key expiry and rotation
MCP keys do not expire. The server performs a bcrypt comparison on each request; agent call rates are well within the 600 req/min rate limit.
Revoking a Doorkeeper access token does not disable an MCP key. To revoke an MCP key, either rotate the client secret or delete the client in Settings → API access. The MCP key is shown once at client creation; regenerate the client secret if you lose it.
Auth error codes
| Status | error.code | When |
|---|
| 401 | invalid_client | Client not found, secret mismatch, non-confidential app, or blank secret |
| 403 | insufficient_scope | The client has no scopes assigned |
Scopes
Scopes control which tools appear in tools/list. A narrower set means fewer tools and less context per session. The full scope list is documented in the Developers → API overview.
The three context tools — get_overview, get_setup_status, and guide — require no scope and are always available to any authenticated client.
Rate limits
600 requests per minute per client. Exceeding the limit returns HTTP 429 with error.code: "rate_limit_exceeded".
Tools are grouped by family below. The "Scope" column shows the required OAuth scope; (any) means no scope is required.
Context tools
| Tool | Scope | Purpose |
|---|
get_overview | (any) | Cheap snapshot of what needs attention. Returns only the sections whose counts are non-zero. Call this first. |
get_setup_status | (any) | Workspace setup snapshot for onboarding and diagnostics. Lists gaps as next_steps to guide the setup skill. |
guide | (any) | Narrative guides for working with Campbooks over MCP. No topic — list of available topics. Pass a topic name for the full guide. |
Email
| Tool | Scope | Purpose |
|---|
list_emails | emails:read | List the most recent emails the caller can access, newest first. Optional filters: unread, query, account, category, priority, date range. |
search_emails | emails:read | Search emails with filters. Uses semantic + keyword blend when a text query is given. |
get_email | emails:read | Fetch a single email by id. format=text (default) returns plain text body, truncated at 8 000 chars. |
send_email | emails:send | Send a new email from one of the caller's connected accounts. |
reply_email | emails:send | Reply to an existing email. Threads from the source message and sends from its account unless overridden. |
mark_email_read | emails:write | Mark an email as read and sync the flag to the provider mailbox. |
mark_email_unread | emails:write | Mark an email as unread (local only). |
add_email_tag | tags:write | Attach an existing workspace tag to an email (by tag_id or name). Tags are not created here — use create_tag. |
remove_email_tag | tags:write | Detach a tag from an email. |
update_emails | emails:write | Bulk-act on emails. archive/unarchive/trash/snooze/unsnooze act on whole threads (mirroring the app UI). |
move_emails_to_folder | emails:write | Move emails (and their full threads) to a folder. Pass folder_name to work cross-account. |
tag_emails | tags:write | Add or remove a tag on a set of emails. The tag must exist — use create_tag to make new ones. |
forward_email | emails:send | Forward an email to another address. |
get_skim_deck | emails:read | Return the Skim inbox deck as compact rings and cluster cards. Apply decisions with skim_decide. |
skim_decide | emails:write | Apply a Skim triage decision to a cluster's emails. Mirrors the inbox UI learning loop (archive, keep, promote). |
Email accounts
| Tool | Scope | Purpose |
|---|
list_email_accounts | email_accounts:read | List connected email accounts visible to the caller. Use the id as email_account_id for filtering. |
connect_email_account | email_accounts:write | Connect a new email account. mode=web returns a URL to open in a browser (normal OAuth flow — recommended for cloud users). mode=token accepts a pre-minted refresh token (self-hosted local OAuth path). |
Documents
| Tool | Scope | Purpose |
|---|
list_documents | documents:read | List the workspace's documents, newest first. Optional filters by document type id and review status. |
get_document | documents:read | Fetch a document by id with its extracted fields and file info (download via the file's download_path). |
upload_document | documents:write | Upload a new document from base64 content. AI classification runs asynchronously. |
update_document | documents:write | Edit a document's extracted fields. Does not change its review state (use approve/reject/reclassify for that). |
approve_document | documents:write | Approve (sign off) a document. |
reject_document | documents:write | Reject a document. |
reclassify_document | documents:write | Change a document's type (also approves it). |
| Tool | Scope | Purpose |
|---|
list_contacts | contacts:read | List the workspace's contacts. Optional text query on name/email and a starred-only filter. |
get_contact | contacts:read | Fetch a single contact by id. |
update_contact | contacts:write | Update a contact's name and/or relationship type. |
set_contact_state | contacts:write | Star/unstar, allow, block, or unblock a contact. |
| Tool | Scope | Purpose |
|---|
list_tags | tags:read | List the workspace's tags (tags apply to emails). |
create_tag | tags:write | Create a new workspace tag. Tags apply to emails and can be used for filtering. |
Document types
| Tool | Scope | Purpose |
|---|
list_document_types | document_types:read | List the workspace's document types (used to classify documents). |
create_document_type | document_types:write | Create a new document type for classifying attachments. |
Folders
| Tool | Scope | Purpose |
|---|
list_folders | folders:read | List the workspace's custom folders. |
get_folder | folders:read | Fetch a folder and the documents filed into it. |
create_folder | folders:write | Create a custom folder. When provision: true, the folder is created on every connected email account as a provider-side label/folder. |
file_document | folders:write | File a document into a folder. |
unfile_document | folders:write | Remove a document from a folder (by membership id). |
Tasks (feature-gated)
Tasks tools require the Tasks feature to be enabled on the server (ENABLE_TASKS). They appear in tools/list only when the feature is on.
| Tool | Scope | Purpose |
|---|
list_tasks | tasks:read | List workspace tasks. Optional status filter; include_archived to see archived tasks. |
get_task | tasks:read | Fetch a task by id with full detail. |
create_task | tasks:write | Create a task in the workspace. |
update_task | tasks:write | Update a task's fields. Status changes use the proper transition (publishes events). |
complete_task | tasks:write | Mark a task as done. |
create_task_from_email | tasks:write | Extract and create a task from an email via the action registry. |
Calendar
| Tool | Scope | Purpose |
|---|
list_calendars | calendar:read | List calendars visible to the caller. Use the id as calendar_id in create_calendar_event. |
list_calendar_events | calendar:read | List calendar events the caller can access, soonest first. Optional start_after / start_before filters. |
get_calendar_event | calendar:read | Fetch a calendar event by id. |
create_calendar_event | calendar:write | Create a calendar event on one of the caller's writable calendars. Times are ISO-8601. |
update_calendar_event | calendar:write | Update a calendar event (you must have write access to its calendar). recurrence_scope: this or all. |
delete_calendar_event | calendar:write | Delete a calendar event (async provider delete). recurrence_scope: this or all. |
rsvp_calendar_event | calendar:write | Set your RSVP on an event (needs_action, accepted, declined, tentative). |
create_event_from_email | calendar:write | Extract and create a calendar event from an email. AI infers the event details; supply overrides as needed. |
Reminders
| Tool | Scope | Purpose |
|---|
list_reminders | reminders:read | List AI-extracted reminders the caller can access. Optional status filter (pending, confirmed, dismissed, snoozed). |
get_reminder | reminders:read | Fetch a reminder by id. |
confirm_reminder | reminders:write | Confirm a reminder into a calendar event. Optionally pass due_at to adjust the time first. |
dismiss_reminder | reminders:write | Dismiss a reminder. |
snooze_reminder | reminders:write | Snooze a reminder until the given time, or one week out when omitted. |
Scheduled emails
| Tool | Scope | Purpose |
|---|
list_scheduled_emails | scheduled_emails:read | List scheduled (and recurring) emails in the workspace, soonest occurrence first. |
get_scheduled_email | scheduled_emails:read | Fetch a scheduled email by id. |
create_scheduled_email | scheduled_emails:write | Schedule an email to send later (optionally recurring via an RRULE). Sends from an account the acting user may send from. |
update_scheduled_email | scheduled_emails:write | Update a pending scheduled email (recipient, subject, body, time, rrule). |
cancel_scheduled_email | scheduled_emails:write | Cancel a scheduled email (soft: sets status to cancelled). |
Scout
| Tool | Scope | Purpose |
|---|
list_scout_threads | scout:read | List the caller's Scout chat threads, newest first. |
create_scout_thread | scout:write | Start a new Scout chat thread. |
list_scout_messages | scout:read | List messages in a Scout thread. Pass after_message_id to poll for the async AI reply. |
send_scout_message | scout:write | Post a user message to a Scout thread. The AI reply is generated asynchronously; poll list_scout_messages with after_message_id until it appears. |
Workflows (feature-gated)
Workflow tools require the Workflows feature to be enabled on the server (ENABLE_WORKFLOWS). They appear in tools/list only when the feature is on.
| Tool | Scope | Purpose |
|---|
list_workflows | workflows:read | List the workspace's automation workflows. |
trigger_workflow | workflows:trigger | Trigger an enabled webhook workflow with an optional JSON payload. |
list_workflow_executions | workflows:read | List a workflow's run history (newest first). |
Templates (feature-gated)
| Tool | Scope | Purpose |
|---|
list_email_templates | templates:read | List the workspace's reusable email templates. |
Feature-gated families
Three tool families are hidden when the corresponding server feature is disabled:
| Family | Server env var | Default |
|---|
| Tasks | ENABLE_TASKS | off |
| Workflows | ENABLE_WORKFLOWS | off |
| Templates | (internal flag) | varies |
These tools simply do not appear in tools/list when the feature is off — they do not return an error.
connect_email_account — web vs token mode
The connect_email_account tool supports two modes:
mode: "web" — returns a relative path to open in a browser. The server's own OAuth flow handles the redirect; this is the correct choice for Campbooks Cloud users and any server whose OAuth callbacks are publicly reachable.
mode: "token" — accepts a pre-minted refresh token directly. Use this for self-hosted instances where the server's OAuth callbacks are not accessible from the public internet. The token must have been minted with the server's own OAuth client credentials (GOOGLE_CLIENT_ID/ZOHO_CLIENT_ID), or the server will fail to refresh it. See Claude Code → Local OAuth for the helper script.
Safety guarantees
Per-user mailbox permissions — an MCP key acts as the user who created it. Mailboxes that user cannot read in the app, the key cannot read. Mailboxes that user cannot send from, the key cannot send from. OAuth scopes are an additional ceiling on top of those permissions, not a replacement.
404-not-403 — resources that exist but belong to a different workspace return 404, not 403. The API and MCP server never reveal whether a resource exists outside your data.
Confirm before send — the /campbooks:triage and /campbooks:setup skills are designed to show the full draft text and wait for an explicit "yes" before calling send_email, reply_email, or forward_email. If you are building your own agent prompt on top of the MCP tools, follow the same pattern: never call a send tool without user approval in the same turn.