forked from mirror/crumbs-original
- Update FEATURES.md, ARCHITECTURE.md, DEPLOYMENT.md with MCP sections - Add MCP endpoints to OpenAPI spec and regenerate API.md - Add rebase-before-PR best practice to CLAUDE.md git conventions - Update README with MCP/API feature mention Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
563 lines
14 KiB
YAML
563 lines
14 KiB
YAML
openapi: 3.1.0
|
|
info:
|
|
title: Crumbs API
|
|
description: REST API for Crumbs by Bretzel — a self-hostable, offline-first notes app.
|
|
version: 0.1.0
|
|
contact:
|
|
name: Bretzel
|
|
url: https://bretzel.app
|
|
|
|
servers:
|
|
- url: http://localhost:3000
|
|
description: Local development
|
|
|
|
security:
|
|
- sessionCookie: []
|
|
|
|
paths:
|
|
/api/auth/setup:
|
|
post:
|
|
summary: First-time password setup
|
|
description: Creates the single user account. Only works once — returns 400 if already set up.
|
|
tags: [Auth]
|
|
security: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [password]
|
|
properties:
|
|
password:
|
|
type: string
|
|
minLength: 8
|
|
responses:
|
|
'201':
|
|
description: Account created, session cookie set
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Success'
|
|
'400':
|
|
description: Setup already completed or invalid password
|
|
|
|
/api/auth/login:
|
|
post:
|
|
summary: Log in
|
|
tags: [Auth]
|
|
security: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [password]
|
|
properties:
|
|
password:
|
|
type: string
|
|
responses:
|
|
'200':
|
|
description: Logged in, session cookie set
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Success'
|
|
'401':
|
|
description: Invalid password
|
|
|
|
/api/auth/logout:
|
|
post:
|
|
summary: Log out
|
|
tags: [Auth]
|
|
responses:
|
|
'200':
|
|
description: Session cleared
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Success'
|
|
|
|
/api/notes:
|
|
get:
|
|
summary: List notes
|
|
tags: [Notes]
|
|
parameters:
|
|
- name: filter
|
|
in: query
|
|
schema:
|
|
type: string
|
|
enum: [all, archived, trashed]
|
|
default: all
|
|
responses:
|
|
'200':
|
|
description: Array of notes
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/Note'
|
|
post:
|
|
summary: Create a note
|
|
tags: [Notes]
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/NoteInput'
|
|
responses:
|
|
'201':
|
|
description: Created note
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Note'
|
|
|
|
/api/notes/{id}:
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
format: uuid
|
|
get:
|
|
summary: Get a note
|
|
tags: [Notes]
|
|
responses:
|
|
'200':
|
|
description: Note with tags
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Note'
|
|
'404':
|
|
description: Note not found
|
|
patch:
|
|
summary: Update a note
|
|
description: Partial update — only include changed fields.
|
|
tags: [Notes]
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/NoteInput'
|
|
responses:
|
|
'200':
|
|
description: Updated note
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Note'
|
|
'404':
|
|
description: Note not found
|
|
delete:
|
|
summary: Permanently delete a note
|
|
tags: [Notes]
|
|
responses:
|
|
'200':
|
|
description: Deleted
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Success'
|
|
'404':
|
|
description: Note not found
|
|
|
|
/api/notes/{id}/attachments:
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
format: uuid
|
|
get:
|
|
summary: List or download attachments
|
|
description: Without `attachmentId`, lists all attachments. With `attachmentId`, returns the file.
|
|
tags: [Attachments]
|
|
parameters:
|
|
- name: attachmentId
|
|
in: query
|
|
schema:
|
|
type: string
|
|
- name: thumb
|
|
in: query
|
|
description: Set to "1" to return the thumbnail instead of the full image (requires attachmentId)
|
|
schema:
|
|
type: string
|
|
enum: ['1']
|
|
responses:
|
|
'200':
|
|
description: Attachment list (without attachmentId) or file content (with attachmentId)
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/Attachment'
|
|
post:
|
|
summary: Upload an image attachment
|
|
tags: [Attachments]
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
multipart/form-data:
|
|
schema:
|
|
type: object
|
|
required: [file]
|
|
properties:
|
|
file:
|
|
type: string
|
|
format: binary
|
|
description: Image file (max 10MB)
|
|
thumbnail:
|
|
type: string
|
|
format: binary
|
|
description: Pre-generated thumbnail (WebP, ~200px)
|
|
responses:
|
|
'201':
|
|
description: Attachment metadata
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Attachment'
|
|
'400':
|
|
description: Invalid file
|
|
delete:
|
|
summary: Delete an attachment
|
|
tags: [Attachments]
|
|
parameters:
|
|
- name: attachmentId
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
responses:
|
|
'200':
|
|
description: Deleted
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Success'
|
|
|
|
/api/search:
|
|
get:
|
|
summary: Full-text search
|
|
description: Searches across note titles, content, and tags. Excludes trashed notes.
|
|
tags: [Search]
|
|
parameters:
|
|
- name: q
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
responses:
|
|
'200':
|
|
description: Matching notes
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/Note'
|
|
|
|
/api/tags:
|
|
get:
|
|
summary: List all tags
|
|
tags: [Tags]
|
|
responses:
|
|
'200':
|
|
description: Array of tags
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/Tag'
|
|
|
|
/api/sync:
|
|
get:
|
|
summary: Pull changes since timestamp
|
|
tags: [Sync]
|
|
parameters:
|
|
- name: since
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
description: Unix timestamp in milliseconds
|
|
responses:
|
|
'200':
|
|
description: Notes updated since the given timestamp
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/Note'
|
|
post:
|
|
summary: Push local changes
|
|
tags: [Sync]
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [changes]
|
|
properties:
|
|
changes:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/SyncChange'
|
|
responses:
|
|
'200':
|
|
description: Changes applied
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Success'
|
|
'400':
|
|
description: Invalid changes payload
|
|
|
|
/api/settings/api-keys:
|
|
get:
|
|
summary: List API keys
|
|
tags: [Settings]
|
|
responses:
|
|
'200':
|
|
description: List of API keys (without hashes)
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/ApiKey'
|
|
post:
|
|
summary: Create API key
|
|
tags: [Settings]
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [name]
|
|
properties:
|
|
name:
|
|
type: string
|
|
responses:
|
|
'201':
|
|
description: API key created (key shown only once)
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: string
|
|
key:
|
|
type: string
|
|
name:
|
|
type: string
|
|
keyPrefix:
|
|
type: string
|
|
|
|
/api/settings/api-keys/{id}:
|
|
delete:
|
|
summary: Revoke API key
|
|
tags: [Settings]
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
responses:
|
|
'200':
|
|
description: Key revoked
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Success'
|
|
'404':
|
|
description: Key not found
|
|
|
|
/api/mcp:
|
|
post:
|
|
summary: MCP Streamable HTTP endpoint
|
|
description: Model Context Protocol endpoint. Accepts JSON-RPC 2.0 messages. Authenticate via Bearer token (API key).
|
|
tags: [MCP]
|
|
security:
|
|
- bearerAuth: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
description: JSON-RPC 2.0 message
|
|
responses:
|
|
'200':
|
|
description: JSON-RPC response (may be SSE stream)
|
|
'400':
|
|
description: Invalid session
|
|
'401':
|
|
description: Invalid or missing API key
|
|
|
|
components:
|
|
securitySchemes:
|
|
sessionCookie:
|
|
type: apiKey
|
|
in: cookie
|
|
name: session
|
|
bearerAuth:
|
|
type: http
|
|
scheme: bearer
|
|
description: API key (crumbs_...)
|
|
|
|
schemas:
|
|
Success:
|
|
type: object
|
|
properties:
|
|
success:
|
|
type: boolean
|
|
|
|
Note:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: string
|
|
format: uuid
|
|
title:
|
|
type: string
|
|
content:
|
|
type: string
|
|
color:
|
|
type: string
|
|
enum: [default, coral, peach, sand, mint, sage, fog, storm, dusk, blossom, clay, chalk]
|
|
pinned:
|
|
type: boolean
|
|
archived:
|
|
type: boolean
|
|
trashed:
|
|
type: boolean
|
|
trashedAt:
|
|
type: string
|
|
format: date-time
|
|
nullable: true
|
|
checklistMode:
|
|
type: boolean
|
|
sortOrder:
|
|
type: integer
|
|
createdAt:
|
|
type: string
|
|
format: date-time
|
|
updatedAt:
|
|
type: string
|
|
format: date-time
|
|
version:
|
|
type: integer
|
|
tags:
|
|
type: array
|
|
items:
|
|
type: string
|
|
attachments:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/Attachment'
|
|
|
|
NoteInput:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: string
|
|
format: uuid
|
|
description: Optional — generated if omitted
|
|
title:
|
|
type: string
|
|
content:
|
|
type: string
|
|
color:
|
|
type: string
|
|
enum: [default, coral, peach, sand, mint, sage, fog, storm, dusk, blossom, clay, chalk]
|
|
pinned:
|
|
type: boolean
|
|
archived:
|
|
type: boolean
|
|
trashed:
|
|
type: boolean
|
|
checklistMode:
|
|
type: boolean
|
|
sortOrder:
|
|
type: integer
|
|
|
|
Tag:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: integer
|
|
name:
|
|
type: string
|
|
|
|
Attachment:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: string
|
|
noteId:
|
|
type: string
|
|
filename:
|
|
type: string
|
|
mimeType:
|
|
type: string
|
|
size:
|
|
type: integer
|
|
thumbnailPath:
|
|
type: string
|
|
nullable: true
|
|
description: Server path to thumbnail file (null if no thumbnail)
|
|
createdAt:
|
|
type: string
|
|
format: date-time
|
|
|
|
SyncChange:
|
|
type: object
|
|
required: [noteId, operation, timestamp]
|
|
properties:
|
|
noteId:
|
|
type: string
|
|
format: uuid
|
|
operation:
|
|
type: string
|
|
enum: [create, update, delete]
|
|
timestamp:
|
|
type: integer
|
|
description: Unix timestamp in milliseconds
|
|
data:
|
|
$ref: '#/components/schemas/NoteInput'
|
|
|
|
ApiKey:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: string
|
|
format: uuid
|
|
name:
|
|
type: string
|
|
keyPrefix:
|
|
type: string
|
|
description: First 12 characters of the key for display
|
|
createdAt:
|
|
type: string
|
|
format: date-time
|
|
lastUsedAt:
|
|
type: string
|
|
format: date-time
|
|
nullable: true
|