DEVELOPER DOCS · MODEL CONTEXT PROTOCOL

Point your agent at your call for speakers

Unsession ships an MCP server, so an AI agent can work your CFP alongside you: read the queue, pull evaluation scores, accept a talk, add a sponsor session, move it on the agenda, chase an outstanding speaker task. It runs the same engines the admin UI does, honours the same permissions, and lands in the same activity log — an agent is just another organizer with a name badge.

MCP ENDPOINT
POST https://unsession.dev/api/mcp
OAuth 2.1 · dynamic client registration — no token needed
Streamable HTTP · stateless JSON-RPC 2.0 · 84 tools

What it is

The Model Context Protocol is how AI agents plug into outside systems. Unsession's MCP server exposes 84 tools — 32 read, 52 write, covering everything an organizer can do in the admin UI — over the hosted service and over any instance you host yourself.

Every tool dispatches into the same functions behind the REST API and the admin screens. There is no parallel implementation to drift: accepting a talk over MCP creates the session, mints the confirmation link and sends the decision email exactly the way the Submissions page does.

1 · Create an API token

Connecting an MCP client? Skip this step. Clients that speak OAuth — claude.ai, Claude Code, Cursor, VS Code and most modern MCP clients — register themselves via dynamic client registration. You sign in on a consent page and never handle a token. Tokens are for the REST API and for clients without OAuth support.

When you do need one, mint it in the admin:

  1. Sign in and open Workspace → API (/app/api). Owners and admins only.
  2. Hit + New token and name it after the agent that will hold it — “Claude Code”, “Program bot”. The name shows up in the activity log on every write it makes.
  3. Pick a scope. Read only for an agent that answers questions about the CFP; Read & write for one that changes things.
  4. Optionally restrict it to one event. Everything outside that event then 404s, which is the safest default when you run several events from one workspace.
  5. Copy the secret. It looks like uns_… and is shown exactly once — there is no recovery, only revoke-and-mint-again.
Sandbox workspaces can’t create tokens. The sandbox personas are shared and throwaway, so token creation is blocked there. Create your own event to use the API.

2 · Connect your agent

Most clients need only the endpoint URL. The server supports OAuth 2.1 with dynamic client registration, so an OAuth-capable client (claude.ai connectors, Claude Code, VS Code, Cursor and most modern MCP clients) registers itself and sends you to a consent page — sign in, pick a workspace and a scope, and the connection appears under API access like any other token. Only a client without OAuth support needs the fallback: a remote HTTP server with a custom header, the URL above plus Authorization: Bearer uns_….

Claude Code

One command — no token:

TERMINAL
claude mcp add --transport http unsession https://unsession.dev/api/mcp

Run /mcp inside Claude Code, pick unsession and choose Authenticate — it registers via OAuth and opens the consent page. Add --scope user to make the server available in every project on your machine instead of just the current one. On a headless machine or in CI, use a static token instead: append --header "Authorization: Bearer uns_your_token_here".

Check it into a repo

For a shared repo, put the server in .mcp.json at the project root. Claude Code expands ${VAR} in both url and headers, so the file is safe to commit — each person exports their own token.

.mcp.json
{
  "mcpServers": {
    "unsession": {
      "type": "http",
      "url": "https://unsession.dev/api/mcp",
      "headers": {
        "Authorization": "Bearer ${UNSESSION_TOKEN}"
      }
    }
  }
}

Claude apps (claude.ai and desktop)

Add it as a custom connector: Settings → Connectors → Add custom connector, paste the endpoint URL, and click Connect. Claude registers itself via OAuth and sends you to the Unsession consent page — sign in, pick the workspace and scope, and you’re connected. No token to paste; the connection shows up under API access and is revoked from there.

Prefer a static token? The Request headers section (beta) still works: add authorization with the value Bearer uns_your_token_here — including the word Bearer and the space, since Claude sends the value exactly as you type it.

Cursor

Project-level .cursor/mcp.json, or ~/.cursor/mcp.json to have it everywhere:

.cursor/mcp.json
{
  "mcpServers": {
    "unsession": {
      "url": "https://unsession.dev/api/mcp",
      "headers": {
        "Authorization": "Bearer ${env:UNSESSION_TOKEN}"
      }
    }
  }
}

VS Code

.vscode/mcp.json, with the token as a prompted input so it never lands in the repo:

.vscode/mcp.json
{
  "inputs": [
    {
      "type": "promptString",
      "id": "unsession-token",
      "description": "Unsession API token",
      "password": true
    }
  ],
  "servers": {
    "unsession": {
      "type": "http",
      "url": "https://unsession.dev/api/mcp",
      "headers": {
        "Authorization": "Bearer ${input:unsession-token}"
      }
    }
  }
}

Any other client

Point it at the endpoint as a Streamable HTTP (sometimes streamable-http) server and give it the Authorization header. To check the credential before you wire anything up, ask the server for its tool list by hand:

TERMINAL
curl -s https://unsession.dev/api/mcp \
  -H "Authorization: Bearer uns_your_token_here" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

A list of tool names comes back as JSON. If you get {"ok":false,"error":"Unknown API token"} with a 401, the secret is wrong or revoked.

3 · Ask for something

Once connected, talk to your agent in plain language — it picks the tools. Things that work well on day one:

Agents work best when you name the event. Most tools take an event argument that accepts the slug or the id, and list_events is how the agent finds it.

Tool reference

Read — any token

ToolWhat it returns
list_eventsEvents this token can see — id, name, slug, dates, timezone, venue, published.
get_eventOne event with its rooms and taxonomies (Track / Format / Level options and their ids).
list_formsAn event’s submission forms — id, name, slug, status, open/close dates, public URL.
get_formOne form in full: settings, open state, and the hydrated field schema with flags and conditions.
list_submissionsSubmissions with answers, speakers, status and resolved track/format/level. Filters: status, form, track, free-text q. Cursor-paginated (default 100, max 500).
get_submissionOne submission in full: answers, speakers, status, evaluation score summary, recent activity.
list_sessionsSessions including schedule (day / start / end / room), type, status and publish flag.
get_sessionOne session — schedule, speakers, track/format, publish flag.
list_speakersSpeaker profiles — name, email, bio, job title, company, pronouns, links, headshot — with task progress counts.
get_speakerOne speaker in full: profile, travel notes, sessions, tasks with latest files, version history.
get_agendaThe published public agenda, same shape as /{slug}/agenda.json. Fails while the agenda is unpublished.
get_schedule_conflictsDouble-booked rooms, speakers in two places, sessions past the day end — per session or all.
list_tasksSpeaker and session task instances with status, due date and target.
list_task_templatesTask templates: type, target, trigger, due rule, clauses, reminders, live instance counts.
preview_task_ruleWho an assignment rule (trigger + clauses) reaches right now, before saving a template.
list_evaluation_plansEvaluation plans: criteria, scope rules, reviewers with per-reviewer load, progress.
list_evaluationsRecorded evaluations — scores per criterion, notes, abstentions. Filters: plan, submission, reviewer.
get_evaluation_scoresScore summary per submission across plans: average, done, expected, remaining.
list_email_templatesEmail templates with subject, body and sent counts.
list_email_logThe email log — every recorded email with status (sent / simulated / failed). Cursor-paginated.
get_emailOne logged email in full, body and failure error included.
get_outboxQueued decisions and task reminders that have not been sent yet.
list_filesThe files library grouped by version chain — deliverables, headshots, samples — with comment counts.
get_fileOne file’s version chain and its cross-role comment thread.
list_embedsSaved website embeds with snippets and URLs, plus the widget/format catalog.
list_content_versionsVersion history for a session’s title/abstract or a speaker profile.
list_activityThe event activity feed — every logged action with actor and detail. Cursor-paginated.
list_contactsThe org-wide speaker CRM directory. Filters: q, company, jobTitle, tag. Org-wide tokens only.
get_contactOne CRM contact: fields, tags, custom fields, notes, cross-event history, pipeline card.
get_pipelineThe speaker-pipeline board — every card by stage in column order.
get_pipeline_cardOne pipeline card with contact, notes and stage history.
list_teamOrg members with roles, plus pending invites.

Write — READ · WRITE tokens only

ToolWhat it does
create_submissionCreate a submission on a form on a speaker’s behalf (organizer-import semantics). Answer keys may be field ids or field labels; unmatched keys are reported back, not stored.
update_submissionUpdate title, abstract and/or answers. Answers merge; a null value removes a key.
decide_submissionSENDS EMAILAccept, decline or waitlist — immediately. Runs the real decision engine: flips the status, creates the public Session on accept, mints a 7-day confirmation link, and emails the speaker unless sendEmail is false.
queue_decisionQueue decisions into the outbox instead — the admin modal’s semantics. Nothing happens (and nothing is visible to speakers) until send_outbox.
send_outboxSENDS EMAILSend the outbox: applies queued decisions (status, sessions, tasks, decision emails) and queued task reminders, 40 rows per call.
remove_from_outboxRemove queued decisions or reminders before they send — the outbox undo.
create_sessionCreate a sponsor or service session. Talk sessions only ever arrive by accepting a submission.
update_sessionSENDS EMAILEdit title, abstract, track/format/level, duration, room, publish flag, sponsor badge, or the slot. Moving a confirmed session emails its speakers a schedule notice and bumps the calendar-file sequence.
schedule_sessionSENDS EMAILPut a session in a slot (day, startMin, optional room) or unschedule it. Same engine, same schedule notice.
delete_sessionDelete a sponsor or service session. Talks can only be unscheduled or unpublished.
auto_scheduleFill the unscheduled bin — deterministic greedy packer. Deliberately sends no schedule emails.
publish_agendaPublish the agenda (or push pending edits live) and bump the public revision.
create_formCreate a form from a preset (cfp, contact, session intake, empty). Starts as a draft.
update_formRename, open/close, set the submission window, and merge settings (welcome copy, notifications, late link…).
update_form_schemaReplace the field list through the builder pipeline: normalize, validate, cascade option renames. Copy-on-write versioning keeps old answers intact.
delete_formDelete a form — refused once it has submissions.
save_evaluation_planSENDS EMAILCreate or update an evaluation plan: criteria, scope rules, reviewers. Newly added reviewers are emailed their queue link.
record_evaluationRecord a score or abstention for a named reviewer — same guards as the reviewer queue; scores are final.
remind_evaluatorsSENDS EMAILEmail evaluators with outstanding reviews, immediately. Reviewers with nothing left are skipped.
update_email_templateEdit a template’s name, subject, body (rich-lite sanitized).
duplicate_email_templateCopy a template as a same-key variant, usable as decide_submission’s templateId.
update_speakerUpdate a speaker profile — name, bio, job title, company, pronouns, links, organizer-only travel notes.
create_speakerAdd a speaker profile to an event (keyed by email, idempotent) and mirror them into the CRM directory.
assign_taskSENDS EMAILAssign a task template to speakers or a session, or a one-off task to one speaker. Already-assigned and no-session speakers are skipped and reported. New assignments email each speaker a digest.
complete_taskMark a task instance done as an organizer override. Idempotent.
remove_taskCancel an open task. Completed tasks are kept for the record.
review_taskSENDS EMAILApprove a pending deliverable, or request changes — which emails the speakers with your message.
queue_task_reminderQueue reminders for one task or all of a speaker’s open tasks; they send as one email from the outbox.
email_speakerSENDS EMAILEmail one speaker directly, immediately, with merge tags.
save_task_templateCreate or edit a task template. Edits pin the old wording onto live instances first.
archive_task_templateArchive (or restore) a template — stops assigning, keeps open instances.
comment_on_fileReply on a file’s comment thread as the organizer. Deliberately sends no email.
restore_content_versionRestore a session or speaker content version — appends a new version, never rewrites history.
create_embedCreate a website embed and get its snippet/URL. Also toggle_embed and delete_embed.
toggle_embedEnable or disable an embed — disabled embeds 404 publicly but keep their config.
delete_embedDelete an embed; its public URL stops working.
create_eventCreate an event with the standard defaults. Org-wide tokens only.
update_eventUpdate event settings and theme — name, slug, dates, timezone, venue, mode, colors.
save_roomAdd or edit a room (name, capacity, priority). delete_room untags sessions first.
delete_roomDelete a room — sessions in it keep their slot, lose the room.
create_taxonomyAdd a taxonomy (option set) with optional per-option color and duration.
save_taxonomy_optionAdd or rename a taxonomy option — renames cascade into form conditions and stored answers.
delete_taxonomy_optionDelete an option — tagged sessions are untagged, never deleted.
save_contactCreate (upsert by email) or update a CRM contact; manage tags and custom fields. Org-wide tokens only.
add_contact_noteNote on a CRM contact’s record.
add_contact_to_eventAdd a CRM contact to an event as a speaker profile — idempotent by email.
email_contactsSENDS EMAILBulk-email directory contacts (max 100 per call) with merge tags.
enroll_pipeline_cardPut a contact on the speaker-pipeline board.
update_pipeline_cardMove a card between stages (history-logged), set score/rationale, add notes.
remove_pipeline_cardTake a card off the board; the contact stays in the directory.
invite_teammateSENDS EMAILInvite a teammate (admin or collaborator) — emails a one-shot accept link.
revoke_inviteRevoke a pending team invite.

The tool surface now matches what an organizer can do in the admin UI. Deliberately still UI-only: file uploads, CRM contact deletion and merging, team role changes and member removal, and CSV / XLSX exports (the API returns the same data as JSON) — interactive or destructive operations that want a human at the wheel. Org-level tools (CRM, pipeline, team) additionally require an org-wide token; event-restricted tokens stay inside their event.

Scopes, side effects & safety

Handing an agent write access to a live CFP is a real decision, so the server is built to make the blast radius legible.

Two ways to decide. queue_decision + send_outbox is the admin UI’s two-phase flow: queueing is invisible to speakers and freely undoable with remove_from_outbox, and nothing happens until the outbox is sent. decide_submission skips the outbox and applies the decision immediately — status flip, session copy, confirmation link and email — because a machine caller is being explicit. Prefer the queue when a human should review before anything reaches a speaker; use a read-only token for an agent that should only recommend.

Host your own

Unsession is AGPL-3.0 and the hosted service at unsession.dev runs this repository unmodified. Self-hosting gives you the same MCP server on your own domain — it is part of the worker, so there is nothing extra to enable, deploy or pay for.

TERMINAL
git clone https://github.com/cvolzer3/unsession
cd unsession
npm install

npx wrangler d1 create unsession-db      # copy the id into wrangler.jsonc
npx wrangler r2 bucket create unsession-files
npx wrangler d1 migrations apply unsession-db --remote
npx wrangler deploy

Set vars.APP_ORIGIN in wrangler.jsonc to the origin your worker answers on, and point routes at your domain (or delete the block to use the workers.dev URL). Then:

The full self-hosting notes — email sending through Cloudflare Email Service, sign-in, local development — are in the repository README.

Protocol details

Only needed if you are writing a client by hand. Everything is JSON-RPC 2.0 over a single POST; the server is stateless, so requests are independent and can be issued in any order.

MethodBehaviour
initializeNegotiates the protocol version (2025-06-18 or 2025-03-26) and returns capabilities { tools: {} } and serverInfo unsession/<version>.
notifications/*Any notification (no id) — including notifications/initialized — gets 202 with an empty body.
pingReturns an empty result.
tools/listLists the tools this token may call. Write tools are omitted entirely for read-only tokens.
tools/callRuns a tool. Results come back as content: [{ type: "text", text: <JSON> }].
EXAMPLE · tools/call
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "list_submissions",
    "arguments": { "event": "devconf-2027", "status": "in_review", "limit": 50 }
  }
}

Troubleshooting

SymptomCause
401 Missing bearer tokenThe header never arrived. Check the client actually forwards custom headers, and that the value starts with Bearer and a space.
401 Unknown API tokenWrong secret, or a token from a different instance. Mint a new one at /app/api.
401 revokedSomeone revoked it. Tokens can’t be un-revoked — create a replacement.
Only the read tools listedRead-only token. The write tools are hidden by design; mint a read-write one.
405 Method not allowedThe client opened a GET/SSE stream. This server is POST-only and stateless.
404 on a known eventThe token is restricted to a different event.
get_agenda failsThat event’s agenda isn’t published yet. Publish it, or read sessions with list_sessions.

The REST API

The same operations, for anything that isn’t an agent — scripts, integrations, a scheduled export. Unlike MCP, REST always authenticates with a token. Base URL https://unsession.dev/api/v1:

TERMINAL
curl -H "Authorization: Bearer uns_your_token_here" https://unsession.dev/api/v1/events

Responses are { ok: true, data } or { ok: false, error }.

Read — any token

RouteWhat it returns
GET /eventsEvents this token can see — id, name, slug, dates, timezone, venue, published.
GET /events/:eventOne event with its rooms and taxonomies (Track / Format / Level options).
GET /events/:event/formsSubmission forms — id, name, slug, status, open/close dates, public URL.
GET /events/:event/submissionsSubmissions with answers and speakers. Filters: status, form, track, free-text q. Cursor-paginated.
GET /submissions/:idOne submission in full: answers, speakers, status, score summary, recent activity.
GET /events/:event/sessionsSessions including schedule (day / start / end / room), type and publish flag.
GET /sessions/:idOne session.
GET /events/:event/speakersSpeaker profiles — bio, pronouns, links, headshot — with task progress counts.
GET /events/:event/agendaThe published agenda, same shape as /{slug}/agenda.json.
GET /events/:event/tasksTask instances with status, due date and speaker/session target.
GET /events/:event/forms/:formOne form in full — settings, open state, hydrated field schema.
GET /speakers/:idOne speaker in full — profile, travel notes, tasks with files, version history.
GET /events/:event/evaluation/plansEvaluation plans with criteria, reviewers and progress.
GET /events/:event/evaluationsRecorded evaluations. Filters: plan, submission, reviewer.
GET /events/:event/evaluation/scoresScore summary per submission across plans.
GET /events/:event/email-templatesEmail templates with sent counts.
GET /events/:event/emailsThe email log, cursor-paginated. GET /emails/:id for one in full.
GET /events/:event/outboxQueued decisions and task reminders awaiting send.
GET /events/:event/filesThe files library by version chain. GET /files/:id for one chain + thread.
GET /events/:event/embedsSaved embeds with snippets, plus the widget/format catalog.
GET /events/:event/task-templatesTask templates with rules and instance counts.
GET /events/:event/agenda/conflictsSchedule conflicts, per session (?session=) or all.
GET /events/:event/activityThe activity feed, cursor-paginated.
GET /content-versions/:subjectType/:idVersion history for a session or speaker.
GET /org/contactsThe CRM directory (org-wide tokens). GET /org/contacts/:id for one.
GET /org/pipelineThe pipeline board. GET /org/pipeline/:id for one card.
GET /org/teamOrg members and pending invites.

Write — READ · WRITE tokens only

RouteWhat it does
POST /events/:event/submissionsCreate a submission on a form on a speaker’s behalf.
PATCH /submissions/:idUpdate title, abstract and/or answers. Answers merge; null removes a key.
POST /submissions/:id/decisionAccept, decline or waitlist immediately. Runs the real decision engine and emails the speaker unless sendEmail is false.
POST /events/:event/outbox/decisionsQueue decisions for the outbox instead (the admin modal’s semantics).
POST /events/:event/outbox/sendSend the outbox — queued decisions then task reminders, 40 rows per call.
POST /events/:event/outbox/removeRemove queued items before they send.
POST /events/:event/sessionsCreate a sponsor or service session. DELETE /sessions/:id removes one.
PATCH /sessions/:idEdit fields. { day, startMin, roomId } schedules; nulls unschedule; published toggles.
POST /events/:event/agenda/autoscheduleFill the unscheduled bin. No emails.
POST /events/:event/agenda/publishPublish the agenda / push edits live.
POST /events/:event/formsCreate a form from a preset. PATCH /forms/:id edits; DELETE removes; PUT /forms/:id/schema replaces the fields.
POST /events/:event/evaluation/plansCreate or update an evaluation plan (emails new reviewers).
POST /events/:event/evaluationsRecord a score or abstention for a named reviewer.
POST /events/:event/evaluation/remindEmail evaluators with outstanding reviews.
PATCH /email-templates/:idEdit a template. POST /email-templates/:id/duplicate copies it.
PATCH /speakers/:idUpdate a speaker profile — name, bio, pronouns, links, travel notes.
POST /events/:event/speakersAdd a speaker profile (keyed by email, idempotent).
POST /tasksAssign a template or one-off task to a speaker or session.
POST /tasks/:id/completeMark a task done as an organizer override. Idempotent.
POST /tasks/:id/removeCancel an open task. POST /tasks/:id/review approves or requests changes (emails).
POST /task-remindersQueue reminders for a speaker’s task(s) into the outbox.
POST /speakers/emailEmail one speaker directly.
POST /events/:event/task-templatesCreate or edit a task template. …/preview previews a rule; /task-templates/:id/archive toggles.
POST /files/:id/commentsReply on a file’s comment thread.
POST /content-versions/:subjectType/:id/restoreRestore a session/speaker content version.
POST /events/:event/embedsCreate an embed. PATCH /embeds/:id toggles; DELETE removes.
POST /eventsCreate an event (org-wide tokens). PATCH /events/:event updates settings and theme.
POST /events/:event/roomsAdd or edit a room. DELETE /events/:event/rooms/:id removes one.
POST /events/:event/taxonomiesAdd a taxonomy. POST …/taxonomy-options adds/renames options (renames cascade); DELETE …/taxonomy-options/:id removes.
POST /org/contactsCreate or update a CRM contact (org-wide tokens). POST /org/contacts/:id/notes, …/add-to-event, /org/contacts/email.
POST /org/pipelineEnroll a contact on the pipeline. PATCH /org/pipeline/:id moves/scores/notes; DELETE removes the card.
POST /org/team/inviteInvite a teammate (emails the accept link). POST /org/team/invites/:id/revoke.

Full request and response shapes live in SPECS/C-api-mcp.md.