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.
- No extra service to run. The endpoint is part of the worker — no SDK, no Durable Objects, no separate process, no additional dependency.
- Stateless. One
POST, one JSON-RPC response. No SSE stream, no session id to keep alive, nothing to reconnect. - Scoped like a person. A token is read-only or read-write, and optionally restricted to one event. A read-only token cannot even see that the write tools exist.
1 · Create an API token
When you do need one, mint it in the admin:
- Sign in and open Workspace → API (
/app/api). Owners and admins only. - 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.
- Pick a scope. Read only for an agent that answers questions about the CFP; Read & write for one that changes things.
- 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.
- Copy the secret. It looks like
uns_…and is shown exactly once — there is no recovery, only revoke-and-mint-again.
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:
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.
{
"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.
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:
{
"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:
{
"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:
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:
- “How many submissions are still in review for DevConf, and which tracks are thinnest?”
- “Show me the five highest-scoring proposals nobody has decided on yet.”
- “Which accepted speakers still haven’t uploaded slides? Assign them the slides task.”
- “Add a 30-minute sponsor session for Acme on day 2 at 14:00 in Studio B.”
- “Move the WASM talk to 11:00 and tell me who that affects.”
- “Diff the published agenda against our website copy and list what’s stale.”
event argument that accepts the slug or the id, and list_events is how the agent finds it.Tool reference
Read — any token
| Tool | What it returns |
|---|---|
| list_events | Events this token can see — id, name, slug, dates, timezone, venue, published. |
| get_event | One event with its rooms and taxonomies (Track / Format / Level options and their ids). |
| list_forms | An event’s submission forms — id, name, slug, status, open/close dates, public URL. |
| get_form | One form in full: settings, open state, and the hydrated field schema with flags and conditions. |
| list_submissions | Submissions with answers, speakers, status and resolved track/format/level. Filters: status, form, track, free-text q. Cursor-paginated (default 100, max 500). |
| get_submission | One submission in full: answers, speakers, status, evaluation score summary, recent activity. |
| list_sessions | Sessions including schedule (day / start / end / room), type, status and publish flag. |
| get_session | One session — schedule, speakers, track/format, publish flag. |
| list_speakers | Speaker profiles — name, email, bio, job title, company, pronouns, links, headshot — with task progress counts. |
| get_speaker | One speaker in full: profile, travel notes, sessions, tasks with latest files, version history. |
| get_agenda | The published public agenda, same shape as /{slug}/agenda.json. Fails while the agenda is unpublished. |
| get_schedule_conflicts | Double-booked rooms, speakers in two places, sessions past the day end — per session or all. |
| list_tasks | Speaker and session task instances with status, due date and target. |
| list_task_templates | Task templates: type, target, trigger, due rule, clauses, reminders, live instance counts. |
| preview_task_rule | Who an assignment rule (trigger + clauses) reaches right now, before saving a template. |
| list_evaluation_plans | Evaluation plans: criteria, scope rules, reviewers with per-reviewer load, progress. |
| list_evaluations | Recorded evaluations — scores per criterion, notes, abstentions. Filters: plan, submission, reviewer. |
| get_evaluation_scores | Score summary per submission across plans: average, done, expected, remaining. |
| list_email_templates | Email templates with subject, body and sent counts. |
| list_email_log | The email log — every recorded email with status (sent / simulated / failed). Cursor-paginated. |
| get_email | One logged email in full, body and failure error included. |
| get_outbox | Queued decisions and task reminders that have not been sent yet. |
| list_files | The files library grouped by version chain — deliverables, headshots, samples — with comment counts. |
| get_file | One file’s version chain and its cross-role comment thread. |
| list_embeds | Saved website embeds with snippets and URLs, plus the widget/format catalog. |
| list_content_versions | Version history for a session’s title/abstract or a speaker profile. |
| list_activity | The event activity feed — every logged action with actor and detail. Cursor-paginated. |
| list_contacts | The org-wide speaker CRM directory. Filters: q, company, jobTitle, tag. Org-wide tokens only. |
| get_contact | One CRM contact: fields, tags, custom fields, notes, cross-event history, pipeline card. |
| get_pipeline | The speaker-pipeline board — every card by stage in column order. |
| get_pipeline_card | One pipeline card with contact, notes and stage history. |
| list_team | Org members with roles, plus pending invites. |
Write — READ · WRITE tokens only
| Tool | What it does |
|---|---|
| create_submission | Create 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_submission | Update title, abstract and/or answers. Answers merge; a null value removes a key. |
| decide_submission | SENDS 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_decision | Queue decisions into the outbox instead — the admin modal’s semantics. Nothing happens (and nothing is visible to speakers) until send_outbox. |
| send_outbox | SENDS EMAILSend the outbox: applies queued decisions (status, sessions, tasks, decision emails) and queued task reminders, 40 rows per call. |
| remove_from_outbox | Remove queued decisions or reminders before they send — the outbox undo. |
| create_session | Create a sponsor or service session. Talk sessions only ever arrive by accepting a submission. |
| update_session | SENDS 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_session | SENDS EMAILPut a session in a slot (day, startMin, optional room) or unschedule it. Same engine, same schedule notice. |
| delete_session | Delete a sponsor or service session. Talks can only be unscheduled or unpublished. |
| auto_schedule | Fill the unscheduled bin — deterministic greedy packer. Deliberately sends no schedule emails. |
| publish_agenda | Publish the agenda (or push pending edits live) and bump the public revision. |
| create_form | Create a form from a preset (cfp, contact, session intake, empty). Starts as a draft. |
| update_form | Rename, open/close, set the submission window, and merge settings (welcome copy, notifications, late link…). |
| update_form_schema | Replace the field list through the builder pipeline: normalize, validate, cascade option renames. Copy-on-write versioning keeps old answers intact. |
| delete_form | Delete a form — refused once it has submissions. |
| save_evaluation_plan | SENDS EMAILCreate or update an evaluation plan: criteria, scope rules, reviewers. Newly added reviewers are emailed their queue link. |
| record_evaluation | Record a score or abstention for a named reviewer — same guards as the reviewer queue; scores are final. |
| remind_evaluators | SENDS EMAILEmail evaluators with outstanding reviews, immediately. Reviewers with nothing left are skipped. |
| update_email_template | Edit a template’s name, subject, body (rich-lite sanitized). |
| duplicate_email_template | Copy a template as a same-key variant, usable as decide_submission’s templateId. |
| update_speaker | Update a speaker profile — name, bio, job title, company, pronouns, links, organizer-only travel notes. |
| create_speaker | Add a speaker profile to an event (keyed by email, idempotent) and mirror them into the CRM directory. |
| assign_task | SENDS 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_task | Mark a task instance done as an organizer override. Idempotent. |
| remove_task | Cancel an open task. Completed tasks are kept for the record. |
| review_task | SENDS EMAILApprove a pending deliverable, or request changes — which emails the speakers with your message. |
| queue_task_reminder | Queue reminders for one task or all of a speaker’s open tasks; they send as one email from the outbox. |
| email_speaker | SENDS EMAILEmail one speaker directly, immediately, with merge tags. |
| save_task_template | Create or edit a task template. Edits pin the old wording onto live instances first. |
| archive_task_template | Archive (or restore) a template — stops assigning, keeps open instances. |
| comment_on_file | Reply on a file’s comment thread as the organizer. Deliberately sends no email. |
| restore_content_version | Restore a session or speaker content version — appends a new version, never rewrites history. |
| create_embed | Create a website embed and get its snippet/URL. Also toggle_embed and delete_embed. |
| toggle_embed | Enable or disable an embed — disabled embeds 404 publicly but keep their config. |
| delete_embed | Delete an embed; its public URL stops working. |
| create_event | Create an event with the standard defaults. Org-wide tokens only. |
| update_event | Update event settings and theme — name, slug, dates, timezone, venue, mode, colors. |
| save_room | Add or edit a room (name, capacity, priority). delete_room untags sessions first. |
| delete_room | Delete a room — sessions in it keep their slot, lose the room. |
| create_taxonomy | Add a taxonomy (option set) with optional per-option color and duration. |
| save_taxonomy_option | Add or rename a taxonomy option — renames cascade into form conditions and stored answers. |
| delete_taxonomy_option | Delete an option — tagged sessions are untagged, never deleted. |
| save_contact | Create (upsert by email) or update a CRM contact; manage tags and custom fields. Org-wide tokens only. |
| add_contact_note | Note on a CRM contact’s record. |
| add_contact_to_event | Add a CRM contact to an event as a speaker profile — idempotent by email. |
| email_contacts | SENDS EMAILBulk-email directory contacts (max 100 per call) with merge tags. |
| enroll_pipeline_card | Put a contact on the speaker-pipeline board. |
| update_pipeline_card | Move a card between stages (history-logged), set score/rationale, add notes. |
| remove_pipeline_card | Take a card off the board; the contact stays in the directory. |
| invite_teammate | SENDS EMAILInvite a teammate (admin or collaborator) — emails a one-shot accept link. |
| revoke_invite | Revoke 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.
- Read-only tokens don’t see write tools. They are filtered out of
tools/listentirely, so the agent never proposes an action it can’t take. If it calls one anyway, the result explains that the token is read-only rather than failing cryptically. - Event-restricted tokens can’t reach past their event. Everything else 404s.
- Every write is on the record. Activity entries name the actor
api:<token name>, so “who moved this session?” has the same answer for an agent as it does for a person. - Emailing tools are labeled. The senders:
decide_submission(suppressible withsendEmail: false),send_outbox(that is its whole job),update_session/schedule_session(a schedule notice, but only when a confirmed session actually moves),assign_taskandsave_evaluation_plan(digests for genuinely new assignments/reviewers),remind_evaluators,review_task(request-changes only),email_speaker,email_contacts, andinvite_teammate. Every other tool is silent — including thequeue_*tools, which only stage work forsend_outbox. - Revoking is instant. Revoke on
/app/apiand the next request gets a 401. The page also shows each token’s last-used timestamp.
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.
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:
- Your MCP endpoint is
https://your-domain/api/mcp— the same path on your origin. - Mint the token on your instance, at
https://your-domain/app/api. Tokens from the hosted service are meaningless on yours and vice versa. - Everything above works unchanged with your URL substituted. Nothing about the MCP server depends on the hosted deployment.
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.
| Method | Behaviour |
|---|---|
| initialize | Negotiates 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. |
| ping | Returns an empty result. |
| tools/list | Lists the tools this token may call. Write tools are omitted entirely for read-only tokens. |
| tools/call | Runs a tool. Results come back as content: [{ type: "text", text: <JSON> }]. |
- Tool failures are results, not errors. A failed tool call returns
isError: truewith a human-readable message in the content, so the agent can read what went wrong and adapt. JSON-RPC error objects are reserved for protocol problems — unparseable body (-32700), malformed request (-32600), unknown method (-32601), unknown tool (-32602). - Notifications get 202 and an empty body. That includes
notifications/initialized. - Batch requests are rejected. Send one request object per POST.
- GET and DELETE return 405. There is no SSE stream to open and no session to delete.
- Auth failures are plain HTTP. A missing, unknown or revoked token gets a 401 with
{ ok: false, error }rather than a JSON-RPC envelope — the request never reaches the protocol layer.
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "list_submissions",
"arguments": { "event": "devconf-2027", "status": "in_review", "limit": 50 }
}
}Troubleshooting
| Symptom | Cause |
|---|---|
| 401 Missing bearer token | The header never arrived. Check the client actually forwards custom headers, and that the value starts with Bearer and a space. |
| 401 Unknown API token | Wrong secret, or a token from a different instance. Mint a new one at /app/api. |
| 401 revoked | Someone revoked it. Tokens can’t be un-revoked — create a replacement. |
| Only the read tools listed | Read-only token. The write tools are hidden by design; mint a read-write one. |
| 405 Method not allowed | The client opened a GET/SSE stream. This server is POST-only and stateless. |
| 404 on a known event | The token is restricted to a different event. |
| get_agenda fails | That 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:
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
| Route | What it returns |
|---|---|
| GET /events | Events this token can see — id, name, slug, dates, timezone, venue, published. |
| GET /events/:event | One event with its rooms and taxonomies (Track / Format / Level options). |
| GET /events/:event/forms | Submission forms — id, name, slug, status, open/close dates, public URL. |
| GET /events/:event/submissions | Submissions with answers and speakers. Filters: status, form, track, free-text q. Cursor-paginated. |
| GET /submissions/:id | One submission in full: answers, speakers, status, score summary, recent activity. |
| GET /events/:event/sessions | Sessions including schedule (day / start / end / room), type and publish flag. |
| GET /sessions/:id | One session. |
| GET /events/:event/speakers | Speaker profiles — bio, pronouns, links, headshot — with task progress counts. |
| GET /events/:event/agenda | The published agenda, same shape as /{slug}/agenda.json. |
| GET /events/:event/tasks | Task instances with status, due date and speaker/session target. |
| GET /events/:event/forms/:form | One form in full — settings, open state, hydrated field schema. |
| GET /speakers/:id | One speaker in full — profile, travel notes, tasks with files, version history. |
| GET /events/:event/evaluation/plans | Evaluation plans with criteria, reviewers and progress. |
| GET /events/:event/evaluations | Recorded evaluations. Filters: plan, submission, reviewer. |
| GET /events/:event/evaluation/scores | Score summary per submission across plans. |
| GET /events/:event/email-templates | Email templates with sent counts. |
| GET /events/:event/emails | The email log, cursor-paginated. GET /emails/:id for one in full. |
| GET /events/:event/outbox | Queued decisions and task reminders awaiting send. |
| GET /events/:event/files | The files library by version chain. GET /files/:id for one chain + thread. |
| GET /events/:event/embeds | Saved embeds with snippets, plus the widget/format catalog. |
| GET /events/:event/task-templates | Task templates with rules and instance counts. |
| GET /events/:event/agenda/conflicts | Schedule conflicts, per session (?session=) or all. |
| GET /events/:event/activity | The activity feed, cursor-paginated. |
| GET /content-versions/:subjectType/:id | Version history for a session or speaker. |
| GET /org/contacts | The CRM directory (org-wide tokens). GET /org/contacts/:id for one. |
| GET /org/pipeline | The pipeline board. GET /org/pipeline/:id for one card. |
| GET /org/team | Org members and pending invites. |
Write — READ · WRITE tokens only
| Route | What it does |
|---|---|
| POST /events/:event/submissions | Create a submission on a form on a speaker’s behalf. |
| PATCH /submissions/:id | Update title, abstract and/or answers. Answers merge; null removes a key. |
| POST /submissions/:id/decision | Accept, decline or waitlist immediately. Runs the real decision engine and emails the speaker unless sendEmail is false. |
| POST /events/:event/outbox/decisions | Queue decisions for the outbox instead (the admin modal’s semantics). |
| POST /events/:event/outbox/send | Send the outbox — queued decisions then task reminders, 40 rows per call. |
| POST /events/:event/outbox/remove | Remove queued items before they send. |
| POST /events/:event/sessions | Create a sponsor or service session. DELETE /sessions/:id removes one. |
| PATCH /sessions/:id | Edit fields. { day, startMin, roomId } schedules; nulls unschedule; published toggles. |
| POST /events/:event/agenda/autoschedule | Fill the unscheduled bin. No emails. |
| POST /events/:event/agenda/publish | Publish the agenda / push edits live. |
| POST /events/:event/forms | Create a form from a preset. PATCH /forms/:id edits; DELETE removes; PUT /forms/:id/schema replaces the fields. |
| POST /events/:event/evaluation/plans | Create or update an evaluation plan (emails new reviewers). |
| POST /events/:event/evaluations | Record a score or abstention for a named reviewer. |
| POST /events/:event/evaluation/remind | Email evaluators with outstanding reviews. |
| PATCH /email-templates/:id | Edit a template. POST /email-templates/:id/duplicate copies it. |
| PATCH /speakers/:id | Update a speaker profile — name, bio, pronouns, links, travel notes. |
| POST /events/:event/speakers | Add a speaker profile (keyed by email, idempotent). |
| POST /tasks | Assign a template or one-off task to a speaker or session. |
| POST /tasks/:id/complete | Mark a task done as an organizer override. Idempotent. |
| POST /tasks/:id/remove | Cancel an open task. POST /tasks/:id/review approves or requests changes (emails). |
| POST /task-reminders | Queue reminders for a speaker’s task(s) into the outbox. |
| POST /speakers/email | Email one speaker directly. |
| POST /events/:event/task-templates | Create or edit a task template. …/preview previews a rule; /task-templates/:id/archive toggles. |
| POST /files/:id/comments | Reply on a file’s comment thread. |
| POST /content-versions/:subjectType/:id/restore | Restore a session/speaker content version. |
| POST /events/:event/embeds | Create an embed. PATCH /embeds/:id toggles; DELETE removes. |
| POST /events | Create an event (org-wide tokens). PATCH /events/:event updates settings and theme. |
| POST /events/:event/rooms | Add or edit a room. DELETE /events/:event/rooms/:id removes one. |
| POST /events/:event/taxonomies | Add a taxonomy. POST …/taxonomy-options adds/renames options (renames cascade); DELETE …/taxonomy-options/:id removes. |
| POST /org/contacts | Create or update a CRM contact (org-wide tokens). POST /org/contacts/:id/notes, …/add-to-event, /org/contacts/email. |
| POST /org/pipeline | Enroll a contact on the pipeline. PATCH /org/pipeline/:id moves/scores/notes; DELETE removes the card. |
| POST /org/team/invite | Invite a teammate (emails the accept link). POST /org/team/invites/:id/revoke. |
Full request and response shapes live in SPECS/C-api-mcp.md.