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.
{
"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: In full mode, the adapter reads the scrumboy_session cookie and loads the user into request context when the cookie is valid. In anonymous mode, cookies are not applied for MCP (same rule as the documented HTTP API boundary).
Bootstrap: If there are no users in the database, authenticated MCP tools are treated as unavailable until bootstrap completes (CountUsers == 0).
Typical codes: AUTH_REQUIRED when a tool needs a signed-in user but the session is missing or invalid. 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. Capabilities (GET /mcp / system.getCapabilities) still run without sign-in so clients can inspect the server.
system.getCapabilities and GET /mcp do not require sign-in; they report auth, bootstrapAvailable, serverMode, and implementedTools.
Use cookie-jar mode for authenticated MCP tools.
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":{}}'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 session →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.
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.