This API is intended for programmatic clients (e.g., agents or integrations), not direct browser use.
This document describes the Model Context Protocol (MCP) HTTP surface implemented under internal/mcp and mounted by the Scrumboy HTTP server. It reflects current behavior only, not a roadmap.
Base path: /mcp (exactly; paths like /mcp/foo return 404).
The MCP adapter is constructed in cmd/scrumboy/main.go with server mode from configuration and registered on the main httpapi server.
GET /mcp- Capabilities discovery (samedataassystem.getCapabilitiesvia POST).POST /mcp- Invoke a single tool.
There are no per-tool URL paths. Every tool is invoked by posting a JSON body to POST /mcp.
Tool names are case-sensitive.
POST body envelope:
{
"tool": "tool.name",
"input": {}
}tool(string, required): registered tool name.input(object, required for tools that decode structured input): pass{}when a tool expects no fields. Omittinginputor sending JSONnullmay cause decoding errors for tools expecting an object.
Unknown top-level fields on the POST body are rejected (strict JSON decode).
Other methods on /mcp return 405 with error code METHOD_NOT_ALLOWED.
Responses use Cache-Control: no-store and Content-Type: application/json; charset=utf-8.
In addition to the /mcp HTTP interface above, Scrumboy exposes a Model Context Protocol (MCP) oriented endpoint that speaks JSON-RPC 2.0. This is a separate transport from the legacy { "tool", "input" } POST body; both are mounted on the same MCP adapter.
Endpoint: POST /mcp/rpc (trailing slash /mcp/rpc/ is accepted).
Intended use: MCP-style clients (e.g. Claude Desktop, agent frameworks) that expect JSON-RPC framing.
- Uses JSON-RPC 2.0.
jsonrpc: must be the string"2.0".method: required (string).id: required for requests that expect a JSON body (initialize,tools/list,tools/call). Omitted for notifications (see below). For parse errors, the response uses"id": nullper JSON-RPC.params: optional forinitializeandtools/list(may be omitted or an object). Fortools/call,paramsmust be a JSON object (see below).
Non-POST requests receive a JSON-RPC error (id null). HTTP status for normal JSON-RPC replies is 200 for both success and error objects in the body (errors are not signaled only by HTTP status). The notifications/initialized (or initialized) notification is answered with 204 No Content and an empty body.
Example request:
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {}
}initialize — Initial handshake. Requires id. Returns protocolVersion (currently 2024-11-05), capabilities, serverInfo, and optional instructions. params may be omitted or empty.
notifications/initialized or initialized — Client acknowledgment after initialize. Must be sent without id (notification). Server responds with 204 and no JSON body. If an id is present, the server rejects the call.
tools/list — Requires id. Returns { "tools": [ ... ] }. Each entry has name, description, and inputSchema (JSON Schema object). Today the list includes four tools with full schemas (projects.list, todos.create, todos.get, todos.update); more entries will be added over time. tools/list does not require a prior initialize.
tools/call — Invokes a tool by name. Requires id and a params object containing:
name(string, required): exact registered tool name (same names asPOST /mcp’stoolfield).arguments(object, optional): tool input; if omitted, treated as{}.
tools/call uses the same tool registry as POST /mcp, so any implemented tool may be invoked even if it does not yet appear in tools/list. For tools that do appear in tools/list, the server performs a lightweight check that JSON Schema required top-level properties are present in arguments before calling the handler; full JSON Schema validation is not performed. Unknown tool names yield JSON-RPC method not found (-32601); optional error.data may include { "name": "<tool>" }.
Example tools/call:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "todos.create",
"arguments": {
"projectSlug": "my-project",
"title": "New todo"
}
}
}All JSON-RPC responses with a body include "jsonrpc": "2.0" and preserve the request id (except parse errors → id: null). This endpoint does not use the legacy ok / data / meta envelope.
Success (tools/call result shape):
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [
{
"type": "json",
"json": {}
}
]
}
}The json object is the tool’s result value (same conceptual payload as legacy data, including nested shapes such as { "todo": { ... } } where the tool returns that).
Error:
{
"jsonrpc": "2.0",
"id": 2,
"error": {
"code": -32602,
"message": "invalid params"
}
}Typical JSON-RPC error codes: -32700 parse error, -32600 invalid request, -32601 method/tool not found, -32602 invalid params / validation, -32603 internal error. The message string is human-readable; some errors include optional data.
- Stateless HTTP: there is no server-side session between requests; behavior does not depend on having called
initializefirst fortools/listortools/call. initializeis supported for clients that expect the handshake; it is not enforced before discovery or tool calls on this server.- Authentication follows the same rules as
/mcp(session cookie and Bearer token infullmode; anonymous mode boundary unchanged). See Authentication and capability model. - The legacy
GET/POST /mcpendpoint remains unchanged and is documented in the sections above.
{
"ok": true,
"data": {},
"meta": {}
}dataholds the tool result (shape varies by tool).metais always a JSON object on success (empty if the tool has no metadata).- List-style tools return their array under
data.itemsunless noted otherwise.
{
"ok": false,
"error": {
"code": "NOT_FOUND",
"message": "not found",
"details": {}
}
}detailsis always present; it is an object when the adapter has nothing to attach ({}).- HTTP status codes generally align with error codes (e.g. 401 for
AUTH_REQUIRED, 403 forCAPABILITY_UNAVAILABLE, 404 forNOT_FOUND), but exact mappings may vary by handler.
Server mode (SCRUMBOY_MODE / config): full or anonymous.
Session (cookie): In full mode, the adapter reads the scrumboy_session cookie and loads the user into request context when the cookie is valid.
Bearer (API access token): In full mode, clients may send Authorization: Bearer <token> using an opaque secret minted via /api/me/tokens (prefix sb_, stored as a hash server-side).
Precedence: If the request includes a Bearer authorization attempt (scheme Bearer per RFC 9110, with the credential in the segment after the first space; trim applies only to that credential string), the adapter validates that token and does not fall back to the session cookie when validation fails. A failed Bearer attempt yields 401 with AUTH_REQUIRED for the entire MCP request (including GET /mcp and system.getCapabilities over GET). If there is no Bearer attempt, the adapter uses the session cookie as before.
Anonymous mode: Session cookies and Bearer tokens are not applied for MCP (same anonymous boundary as the documented HTTP API for cookies).
Bootstrap: If there are no users in the database, authenticated MCP tools are treated as unavailable until bootstrap completes (CountUsers == 0).
Capabilities auth object: Field mode keeps the existing meaning (sessionCookie or disabled). Field authMethods (e.g. ["sessionCookie","bearer"] in full mode) lists mechanisms the adapter supports; clients should not treat mode as an exhaustive list of auth options.
Typical codes: AUTH_REQUIRED when the transport rejects the principal (failed Bearer, or tool needs a signed-in user but none is in context) or when a tool requires sign-in without a session/API token. CAPABILITY_UNAVAILABLE when the server is in anonymous mode, or before bootstrap (no users yet), or the tool is otherwise gated as unavailable.
Practical rule: Almost all project-scoped tools (todos, sprints, tags, members, board) require full mode, post-bootstrap, and a valid session or valid API bearer token. When no Authorization: Bearer header is sent, GET /mcp / system.getCapabilities still run without sign-in so clients can inspect the server; if a Bearer attempt is present and invalid, that rule does not apply - the request fails at 401 first.
Use cookie-jar mode for authenticated MCP tools, or a bearer token (see API access tokens).
If the server is not bootstrapped yet (no users), create the first user:
curl -c cookies.txt -X POST http://localhost:8080/api/auth/bootstrap \
-H "Content-Type: application/json" \
-H "X-Scrumboy: 1" \
-d '{"email":"user@example.com","password":"password","name":"User"}'If users already exist, log in:
curl -c cookies.txt -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-H "X-Scrumboy: 1" \
-d '{"email":"user@example.com","password":"password"}'Then call MCP with the session cookie:
curl -b cookies.txt -X POST http://localhost:8080/mcp \
-H "Content-Type: application/json" \
-d '{"tool":"projects.list","input":{}}'When the server is configured with OIDC environment variables (SCRUMBOY_OIDC_ISSUER, SCRUMBOY_OIDC_CLIENT_ID, SCRUMBOY_OIDC_CLIENT_SECRET, SCRUMBOY_OIDC_REDIRECT_URL), two additional endpoints become available:
| Method | Path | Purpose |
|---|---|---|
GET |
/api/auth/oidc/login?return_to=/ |
Redirects browser to the IdP authorization endpoint |
GET |
/api/auth/oidc/callback?code=...&state=... |
Handles the IdP callback, creates a session, and redirects to return_to |
These are browser-redirect endpoints, not JSON APIs. After successful OIDC login, the user receives a standard scrumboy_session cookie. MCP and REST access work identically to password-based sessions.
GET /api/auth/status includes oidcEnabled (bool) and localAuthEnabled (bool) when OIDC is configured.
Manage opaque MCP/API tokens while logged in (session cookie). Mutating endpoints require X-Scrumboy: 1 like other /api writes.
| Method | Path | Body | Success |
|---|---|---|---|
GET |
/api/me/tokens |
— | 200 JSON { "items": [ { "id", "name?", "createdAt", "lastUsedAt?", "revokedAt?" } ] } (no secret) |
POST |
/api/me/tokens |
{ "name": "optional label" } |
201 JSON { "id", "name?", "createdAt", "token" } — token is shown only on create |
DELETE |
/api/me/tokens/{id} |
— | 204 (revoke / soft-delete) |
Create a token (after login, with session + header):
curl -b cookies.txt -X POST http://localhost:8080/api/me/tokens \
-H "Content-Type: application/json" \
-H "X-Scrumboy: 1" \
-d '{"name":"Claude"}'Then call MCP with Bearer (no cookie required for this path):
curl -X POST http://localhost:8080/mcp \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sb_paste_token_from_create_response" \
-d '{"tool":"projects.list","input":{}}'Tools use these public identifiers as primary keys in inputs and outputs:
- Project:
projectSlug - Todo:
projectSlug+localId(no global todo id in MCP todo/board shapes) - Sprint:
projectSlug+sprintId-sprintIdis the stored sprint row id (see sprint list/get); sprint payloads also includenumberfor display ordering - Mine-scope tag:
tagId(current user’s tag library) - Project-scope tag:
projectSlug+tagId(tag row scoped to that project; not user-owned) - Project member / membership target:
projectSlug+userId - Available user (invite list):
userId(frommembers.listAvailable)
system.getCapabilities includes an identity object echoing some of these patterns.
Note: projects.list returns projectId on each item in addition to projectSlug. MCP mutations still key off projectSlug. projectId is returned for informational purposes only and is not used as an input identifier in MCP tools.
Grouped by domain. All are listed in implementedTools from capabilities.
system
system.getCapabilities- Server mode, auth snapshot, identity/pagination hints, full tool list.
projects
projects.list- Projects visible to the user (with role).
board
board.get- Paged board view per workflow column (special pagination; see below).
todos
todos.create,todos.get,todos.search,todos.update,todos.delete,todos.move
sprints
sprints.list,sprints.get,sprints.getActive,sprints.create,sprints.activate,sprints.close,sprints.update,sprints.delete
tags
tags.listProject,tags.listMine,tags.updateMineColor,tags.deleteMine,tags.updateProjectColor,tags.deleteProject
members
members.list,members.listAvailable,members.add,members.updateRole,members.remove
Planned tools: none exposed in capabilities today (plannedTools omitted when empty).
Conventions:
- Inputs use camelCase JSON keys matching the Go structs; unknown keys are rejected where
decodeInputis used. - Auth gates omitted below repeat: anonymous mode →
CAPABILITY_UNAVAILABLE; pre-bootstrap →CAPABILITY_UNAVAILABLE; no authenticated principal (no valid session or API token on the request) →AUTH_REQUIREDfor tools that require it.
- Purpose: Describe server, auth, identities, pagination notes, and implemented tools.
- Input:
{}(use empty object for POST). - Output:
data= capabilities object:serverMode,auth,bootstrapAvailable,identity,pagination,implementedTools, optionalplannedTools. - Meta: e.g.
adapterVersion(integer). - Example (GET or POST):
POST /mcp{"tool":"system.getCapabilities","input":{}}
→ok: true,data.implementedTools= full tool array.
- Purpose: List projects for the current user with role.
- Input:
{} - Output:
data.items- array of projects (projectSlug,projectId,name,image,dominantColor,defaultSprintWeeks,expiresAt,createdAt,updatedAt,role).
- Purpose: Board snapshot with optional tag/search/sprint filters and per-column pagination.
- Input:
projectSlug(required); optionaltag,search,sprintId(sprint row id; must belong to the project when set); optionallimit(default 20, max 100); optionalcursorByColumn(map column key → opaque cursor string). OmittingsprintIdapplies no sprint-based filter on the board query (internal modenone). - Output:
data.project(projectSlug,name,role),data.columns(each:key,name,isDone,itemsas todo-shaped objects). - Meta:
nextCursorByColumn,hasMoreByColumn,totalCountByColumn(per column key). See Board pagination below. - Note: Not available in anonymous mode or before bootstrap; requires sign-in.
| Tool | Input (summary) | Output (summary) |
|---|---|---|
todos.create |
projectSlug, title, optional body, tags, columnKey, estimationPoints, sprintId, assigneeUserId, position |
data.todo |
todos.get |
projectSlug, localId |
data.todo |
todos.search |
projectSlug, query, optional limit, excludeLocalIds |
data.items (lightweight search hits) |
todos.update |
projectSlug, localId, patch (JSON patch object) |
data.todo |
todos.delete |
projectSlug, localId |
data with status: "deleted", projectSlug, localId |
todos.move |
projectSlug, localId, toColumnKey, optional afterLocalId, beforeLocalId |
data.todo |
Column keys accept common aliases (normalized internally). Todo payloads use localId and projectSlug; they do not expose the internal global todo id.
Shared inputs: many tools use projectSlug only or projectSlug + sprintId (stored id).
| Tool | Input | Output |
|---|---|---|
sprints.list |
projectSlug |
data.items (sprint rows + counts), meta.unscheduledCount |
sprints.get |
projectSlug, sprintId |
data.sprint |
sprints.getActive |
projectSlug |
data.sprint - sprint object or JSON null when there is no active sprint |
sprints.create |
projectSlug, name, plannedStartAt, plannedEndAt (ISO-8601 strings) |
data.sprint |
sprints.activate |
projectSlug, sprintId |
data.sprint |
sprints.close |
projectSlug, sprintId |
data.sprint (closed) |
sprints.update |
projectSlug, sprintId, patch |
data.sprint |
sprints.delete |
projectSlug, sprintId (maintainer+) |
data with status: "deleted", projectSlug, sprintId |
Activate/close enforce sprint state (e.g. planned vs active); violations return VALIDATION_ERROR with details.
| Tool | Input | Output |
|---|---|---|
tags.listProject |
projectSlug |
data.items (tagId, name, count, color, canDelete) |
tags.listMine |
{} |
data.items (mine tags; no count) |
tags.updateMineColor |
tagId, color (hex or null to clear) |
data.tag |
tags.deleteMine |
tagId |
data.deleted { tagId } - only if tag is in the viewer’s mine list, then store delete |
tags.updateProjectColor |
projectSlug, tagId, color |
data.tag - maintainer+; tag must be project-scoped in that project |
tags.deleteProject |
projectSlug, tagId |
data.deleted { projectSlug, tagId } - maintainer+; tag must exist as a project-scoped tag in that project |
| Tool | Input | Output |
|---|---|---|
members.list |
projectSlug |
data.items (member rows with normalized roles where implemented) |
members.listAvailable |
projectSlug |
data.items (users not in project) - maintainer+ |
members.add |
projectSlug, userId, role (maintainer | contributor | viewer only) |
data.member |
members.updateRole |
projectSlug, userId, role (same three) |
data.member |
members.remove |
projectSlug, userId |
data.removed { projectSlug, userId } |
Member list payloads normalize legacy role strings where the adapter applies mapping (owner→maintainer, editor→contributor).
members.updateRole: self-demotion and last-maintainer demotion → CONFLICT.
members.remove: last maintainer removal → VALIDATION_ERROR (store mapping).
This is not a single cursor for the whole board.
limit: Maximum todos returned per workflow column (default 20, clamped 1-100).cursorByColumn: Map from column key (string) to an opaque cursor token (base64url). Cursors are produced by the server; clients should not parse them.meta.nextCursorByColumn: Per-column next cursor, ornullwhen there is no next page.meta.hasMoreByColumn: Whether more todos exist in that column for the same filters.meta.totalCountByColumn: Total matching todos in that column (independent of the current page).
Invalid column keys in cursorByColumn or malformed cursors → VALIDATION_ERROR with field hints.
The web app and other REST clients use this endpoint (separate from MCP). In full mode it requires a valid session cookie or Authorization: Bearer API token.
Query parameters:
limit(optional): page size; default 20, maximum 100.sort(optional):activity(default) orboard. Invalid or empty values are treated asactivity(backward compatible).cursor(optional): pagination token from the previous JSON response’snextCursorfield.
Activity sort (default): rows are ordered by updated_at DESC, id DESC. The cursor is updatedAtMs:id (two integers, colon-separated, Unix ms for the todo’s updated_at).
Board sort (sort=board): rows are ordered by project_id ASC, workflow column position ASC, rank ASC, id ASC, matching board order within each project. Cross-project order follows numeric project id, not name or recency. The cursor is projectId:wcPosition:rank:todoId (four integers, colon-separated).
A cursor that does not match the selected sort (for example, an activity cursor while sort=board) is rejected with HTTP 400 and error code VALIDATION_ERROR.
AUTH_REQUIRED- Sign-in required (including some store unauthorized paths mapped from the store layer).CAPABILITY_UNAVAILABLE- Anonymous server mode, pre-bootstrap, or a tool that is unavailable in the current mode.NOT_FOUND- Unknown tool name, or resource not found in the requested scope.FORBIDDEN- Authenticated but not allowed (e.g. role too low for the operation).VALIDATION_ERROR- Invalid JSON input, missing fields, invalid values, or store validation (e.g. sprint state, last-maintainer removal rules).CONFLICT- Store-reported conflict (e.g. duplicate member, role demotion rules).INTERNAL- Unexpected server or store failure.METHOD_NOT_ALLOWED- Any HTTP method other thanGETorPOSTon/mcp.
Some handlers return FORBIDDEN with a clear message where mapStoreError would map the same store error to AUTH_REQUIRED; both patterns exist in the current code.
- Public identifiers first: Mutations and reads are keyed by
projectSlug,localId, and similar fields - not internal numeric ids for todos or projects in MCP command shapes (exceptprojectIdon list output as noted). - Capabilities match implementation:
implementedToolsis the authoritative list of POST tool names. - Narrower than REST: Some MCP tools intentionally pre-check scope (e.g. mine-tag delete via library membership) or map errors deterministically; behavior may differ from every REST edge case.
- Anonymous MCP: Tag, member, board, todo, and sprint tools are not offered in anonymous server mode through MCP (
CAPABILITY_UNAVAILABLE), even if anonymous boards exist elsewhere in the product.