AI Assistant
Connect einktodo to MCP-aware AI clients — Claude Desktop, Cursor, or anything that speaks MCP — over one URL and a single bearer token. The assistant can then read your e-ink devices, design what shows on the screen, and manage your todos by chat.
Two servers, one per job
einktodo ships two independent MCP servers. Each has its own endpoint and its own kind of API key, so a client only ever sees the tools it is meant to use.
| Server | Endpoint | API key scope | Tools |
|---|---|---|---|
| Todo MCP | https://www.einktodo.com/api/mcp/todo | legacy | Read and edit your todo list. |
| Display MCP | https://www.einktodo.com/api/mcp/display | agent | Inspect devices and control what the e-ink screen shows. |
Both use Streamable HTTP (SSE is not enabled) and authenticate with Authorization: Bearer <your-api-key>.
Connect one or both — they are ordinary MCP servers and clients happily run them side by side. The two generators at the bottom of this page create the right key and emit a ready-to-paste config for each.
Using the wrong key against a server returns scope_forbidden naming the scope it wants; a legacy key on the Display MCP (or vice versa) never silently half-works.
Deprecated: the older combined endpoint
https://www.einktodo.com/api/mcpstill serves all the tools from both servers, gated by your key's scope. Existing configs keep working, but new setups should point at one of the two endpoints above.
Todo MCP (legacy scope)
Every tool is scoped to you: an id belonging to someone else reads as not_found.
| You can ask the assistant to… | Tool |
|---|---|
| See or filter your todos ("what's open today?") | list_todos (supports query, tag, completed, pagination) |
| Look up one todo by id | get_todo |
| Add a todo ("remind me to buy milk") | create_todo |
| Edit a todo (title, note, due date, tags) | update_todo |
| Mark a todo done or not done | set_todo_completed |
| Delete a todo | delete_todo |
| Add/update several at once | batch_todos |
batch_todos runs every item in a single transaction. Items with an id update that todo; items without one create it. title is required on every item, even when you are only changing something else.
Prompt — triage-todos. Reviews open todos, groups them by urgency from due_date/tags, flags overdue items, and proposes edits to apply after your confirmation.
Display MCP (agent scope)
Every display tool accepts an optional device_id: omit it if you have only one device or your key is bound to one; pass it when you have multiple. If the key is bound to a single device, any mismatching device_id returns device_locked. With multiple devices and no binding, device_id is required (the error lists the available ids).
list_devices
List the user's e-ink devices and their display geometry. No arguments.
Each device also reports its current display state:
| Field | Type | Description |
|---|---|---|
id, name, width, height, colors | — | Identity and geometry. colors is bw or bwr. |
last_seen, firmware_version | string? | Last poll time (ISO) and reported firmware. |
display_mode | "todo" | "agent" | "image" | Which mode the device is currently rendering. |
has_agent_content | boolean | Whether a payload has been pushed (i.e. whether agent mode is available). |
has_image | boolean | Whether an image is configured (i.e. whether image mode is available). |
get_display_content
Read the device's current agent-pushed payload. Mirrors GET /api/agent/display.
| Argument | Type | Description |
|---|---|---|
device_id | string? | Target device. |
Returns { payload, updated_at, created_at, device_id }. payload is null (and the timestamps too) when nothing has been pushed.
push_display_content
Push a layout payload to an e-ink device and switch it into agent mode. Same schema as POST /api/agent/display.
| Argument | Type | Description |
|---|---|---|
payload | object | { device_id?, background?, root } — see the display payload schema Resource described below. |
Returns { ok, updated_at, device }. Invalid payloads return invalid_payload with the validator's reason.
clear_display
Remove the agent-pushed payload from a device and revert display_mode to todo.
| Argument | Type | Description |
|---|---|---|
device_id | string? | Target device. |
preview_display
Render the device's current agent content to a PNG and return it as an inline image, so the model can see what the e-ink screen actually shows. Reuses the same renderer as GET /api/agent/display/preview.
| Argument | Type | Description |
|---|---|---|
device_id | string? | Target device. |
Returns an image content block (no structuredContent). When nothing is pushed it returns a blank frame at the device's geometry plus a note suggesting push_display_content first.
upload_image
Upload a PNG or JPEG so it can be referenced from a display payload. Returns { image_id, width, height, mime_type }.
| Argument | Type | Description |
|---|---|---|
data_base64 | string | Base64-encoded image bytes. Accepts a bare base64 string or a data URL (data:image/png;base64,...). Max ~5MB decoded. |
mime_type | "image/png" | "image/jpeg"? | Optional hint for the stored key's extension; the actual bytes are still validated with sharp. |
Limits: max ~5MB, max 2000×2000. Only PNG and JPEG are accepted.
Image flow. image payload elements reference an image by image_id, not a URL. So to put a picture on the screen you: (1) call upload_image to get an image_id, then (2) include { "type": "image", "image_id": "<that id>" } in the root of a push_display_content payload. A payload that references an unknown/deleted image_id renders a dashed placeholder. (The REST equivalent, POST /api/agent/image, takes a multipart file field instead of base64 — see below.)
set_display_mode
Explicitly choose which mode a device renders. Today the only implicit switches are push_display_content → agent and clear_display → todo; this tool lets you flip between all three modes directly. To use image mode you must first configure an image with set_display_image (below).
| Argument | Type | Description |
|---|---|---|
device_id | string? | Target device. |
mode | "todo" | "agent" | "image" | The mode to switch to. |
Availability is validated before the switch:
| Mode | Renders | Availability rule |
|---|---|---|
todo | The built-in todo list | Always available. |
agent | The agent-pushed layout | Requires content pushed via push_display_content (i.e. has_agent_content: true). |
image | The configured image | Requires an image to be configured on the device (imageConfig.s3Key, i.e. has_image: true). |
If the mode is unavailable the call returns mode_unavailable explaining what to do first (push content for agent, configure an image with set_display_image for image). On success returns { ok: true, device_id, mode }.
set_display_image
Configure the full-screen picture shown in image mode. This is the prerequisite that makes set_display_mode("image") available. The image is uploaded through the same processing pipeline as the web flow (resized/optimized for the e-ink screen) and stored as DeviceConfig.imageConfig.
| Argument | Type | Description |
|---|---|---|
device_id | string? | Target device. |
data_base64 | string | Base64 PNG/JPEG (bare or a data: URL). Max ~5MB; resized server-side. |
fill_mode | "cover" | "contain" | "stretch"? | How the image fills the screen. Default "cover". |
activate | boolean? | Also switch the device to image mode now. Default true; pass false to stage the image without switching. |
Returns { ok: true, device_id, mode, image: { s3_key, url, fill_mode } }. Returns storage_unavailable if object storage is not configured, or invalid_image / file_too_large for bad input.
Note the two distinct "image" concepts:
upload_imageproduces animage_idfor animageelement inside anagentlayout (the Image flow above);set_display_imagesets the full-screen background forimagedisplay mode. They are independent.
Display REST endpoints (scope agent)
The display tools map onto these REST routes if you prefer plain HTTP:
| Method + Path | Purpose |
|---|---|
GET /api/agent/display | Read the current payload → { payload, updated_at, created_at } (or { payload: null }). |
POST /api/agent/display | Push a payload (body is the { device_id?, background?, root } payload) and switch to agent mode. |
DELETE /api/agent/display | Clear the payload and revert to todo mode. |
GET /api/agent/display/preview | Render the current agent content and return a PNG buffer. |
POST /api/agent/image | multipart/form-data with a single file field (PNG/JPEG). Returns { image_id, width, height }. |
DELETE /api/agent/image?image_id=... | Remove the R2 object and DB row. |
device_id is taken from the request body (POST) or ?device_id= query (GET/DELETE), with the same locking/required rules as the MCP tools.
Resources, prompts & structured output
Both servers expose more than tools.
Resource — display payload schema (Display MCP). einktodo://schema/display-payload (MIME application/json) serves the JSON Schema for the push_display_content payload, so a client can fetch the exact element shape instead of guessing. The root must be a single element of type box, text, image, rect, line, list, or divider. Beyond the schema, the server enforces: max depth 16, max 500 nodes, max 50000 total text characters. Sizes are number | "auto" | "fill"; colors are black | red | white (use red only on bwr devices); image elements reference an image_id from upload_image.
Prompts. One per server, to bootstrap the common flow:
| Prompt | Server | Args | What it does |
|---|---|---|---|
daily-dashboard | Display | device_id? | Composes a "Today" dashboard: gathers open todos, reads the payload-schema Resource, builds a single-column layout, and pushes it. With the Todo MCP also connected it pulls the list itself via list_todos; otherwise it asks you for the items. |
triage-todos | Todo | — | Reviews open todos, groups by urgency from due_date/tags, flags overdue items, and proposes edits to apply with confirmation. |
Structured output. Most tools declare an outputSchema and return a structuredContent block alongside the human-readable text, so clients that support MCP structured output get a typed object (e.g. { devices: [...] }, { todo: {...} }, { image_id, width, height, mime_type }) instead of having to parse the text. The text content stays backward-compatible. preview_display is the exception: it returns an inline image content block and intentionally declares no outputSchema.
Todo MCP config
Sign in to generate a config block with your API key already filled in.
Sign In{
"mcpServers": {
"einktodo-todo": {
"url": "https://www.einktodo.com/api/mcp/todo",
"headers": {
"Authorization": "Bearer YOUR_API_KEY"
}
}
}
}Display MCP config
Sign in to generate a config block with your API key already filled in.
Sign In{
"mcpServers": {
"einktodo-display": {
"url": "https://www.einktodo.com/api/mcp/display",
"headers": {
"Authorization": "Bearer YOUR_API_KEY"
}
}
}
}