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 · 19 tools

What it is

The Model Context Protocol is how AI agents plug into outside systems. Unsession's MCP server exposes 19 tools — ten read, nine write — 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.
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_agendaThe published public agenda, same shape as /{slug}/agenda.json. Fails while the agenda is unpublished.
list_tasksSpeaker and session task instances with status, due date and target.

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. 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.
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.
update_speakerUpdate a speaker profile — name, bio, job title, company, pronouns, links. May auto-complete an open “complete profile” task.
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.

Deliberately out of scope in v1: form and schema editing, event creation, team management, and email template CRUD. Those have versioning and permission semantics that deserve their own design rather than a tool call.

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.

Decisions over MCP skip the outbox. In the admin UI, deciding a submission only queues the decision for review, and an organizer sends it from Emails → Outbox. The decide_submission tool applies the decision immediately — status flip, session copy, confirmation link and email — because a machine caller is being explicit. Use a read-only token if you want an agent that can recommend decisions but not make them.

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 ten tools listedRead-only token. The nine 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.

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. Runs the real decision engine and emails the speaker unless sendEmail is false.
POST /events/:event/sessionsCreate a sponsor or service session.
PATCH /sessions/:idEdit fields. { day, startMin, roomId } schedules; nulls unschedule; published toggles.
PATCH /speakers/:idUpdate a speaker profile — name, bio, pronouns, links.
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.

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