Hi folks,
A preview of the Checkvist MCP server with OAuth authentication is available at https://beta.checkvist.com/mcp
Try it out, tell us how it works, what’s missing, what’s wrong.
The MCP covers most of our OpenAPI features.
Thank you!
Hi folks,
A preview of the Checkvist MCP server with OAuth authentication is available at https://beta.checkvist.com/mcp
Try it out, tell us how it works, what’s missing, what’s wrong.
The MCP covers most of our OpenAPI features.
Thank you!
I’m getting an error message:
{“error”:“Authentication required: pass ‘Authorization: Bearer \u003cCheckvist API token\u003e’ or connect via OAuth”}
That’s great! First look: 30 tools, serverInfo: checkvist 1.0, streamable HTTP. Full OAuth 2.1 stack is implemented, but it also accepts Authorization: Bearer <remote_key>.
Thanks!
I got a note from an agent.
Feedback on the Checkvist MCP preview (beta.checkvist.com/mcp)
Tested 2026-07-25 against a live Pro account, protocol 2025-06-18, serverInfo: checkvist 1.0, 30 tools (14 read-only, 16 mutating). I maintain a Kotlin REST client against your OpenAPI spec, so the comparison below is against that.
/.well-known/oauth-protected-resource and /.well-known/oauth-authorization-server are both served, with dynamic client registration, PKCE-S256 and read_only/read_write scopes. That’s more than most MCP servers ship, and it means generic clients can connect without hand-configuration.instructions field earns its place. Telling the client “call get_checklist_stats first to gauge size, then get_tasks_summary” is exactly the kind of guidance that stops an agent from pulling a 2000-item list into context.get_checklist_stats / get_tasks_summary / get_tasks_paginated / list_top_level_tasks have no OpenAPI equivalent, and they’re the difference between an agent that can work with a big list and one that can’t.export_checklist + import_tasks as an OPML round-trip is a genuinely good transport design, and the tool descriptions say so explicitly (“round-trips losslessly”). Also good: the markdown export description warns it contains no task ids and points at get_tasks_summary instead. That kind of cross-reference in a description is what makes a tool set usable.1. destructiveHint is inverted on every read-only tool. All 14 read tools declare readOnlyHint: true together with destructiveHint: true, while every create/update declares destructiveHint: false:
checkvist_get_current_user readOnly=True destructive=True ← contradictory
checkvist_search_tasks readOnly=True destructive=True ← contradictory
checkvist_list_checklists readOnly=True destructive=True ← contradictory
checkvist_update_task readOnly=False destructive=False
checkvist_create_task readOnly=False destructive=False
checkvist_import_tasks readOnly=False destructive=False ← see below
checkvist_delete_task readOnly=False destructive=True ← correct
checkvist_delete_checklist readOnly=False destructive=True ← correct
The spec says destructiveHint is only meaningful when readOnlyHint is false, so on the read tools it’s at best noise — but a client that gates auto-approval on annotations will prompt on every single read while silently approving writes. The delete tools are annotated correctly, so this looks like a default that was applied the wrong way round rather than a misunderstanding.
2. import_tasks with replace_existing: true deletes the list’s entire contents and is annotated destructive: false. By its own description it’s the most dangerous tool in the set. It should be destructive: true, and arguably replace_existing deserves a confirmation-shaped design of its own — an agent that misreads “import” as additive will silently destroy a list.
3. The MCP endpoint accepts the remote key as bearer, but not a token from auth/login.json. I sent a valid session token both as Authorization: Bearer and as X-Client-Token; both 401. So an existing API client can’t reuse the token it already holds — it has to keep the remote key around separately. Is the raw-remote-key path intended and supported, or a preview shortcut that will disappear in favour of OAuth only? That determines whether I can wire this up permanently.
4. Tools return markdown text only — no structuredContent. get_current_user comes back as a JSON blob inside a fenced code block in a text content item. Since you’re on 2025-06-18, outputSchema + structuredContent are available, and ids in particular (checklist_id, task_id) would be far more reliable read from structured output than parsed back out of rendered markdown. The instructions field currently has to explain how to scrape ids out of the prose, which is a symptom of this.
Measured against your own OpenAPI spec and my client:
list_checklists has an archived filter, so archived lists are visible but unreachable — you can see them and not act on them.update_checklist can’t set tags, although create_checklist can. So list tags are write-once.import_tasks drops most of the REST import surface: position, status, separate_with_empty_line, import_content_note, and the tag:<name> list selector (importing into “the list tagged X” without knowing its id — useful precisely for an agent that doesn’t hold ids).get_tasks has no order parameter.GET /tasks/{id} returns the item followed by its ancestors; MCP has get_task_tree (descendants) but nothing that answers “where does this item sit”. For an agent working from search_tasks results — which return items without their context — that’s the missing half.It’s life chaging, it helped me go sort lists that are a big mess and prioritize my priorities.
Also right now I can really treat my list as a personal wiki (advanced search). I’m using it with Cursor AI editor.
Here is more agent-generated feedback after implementing some of my use cases. (Opus 5)
Follow-up: findings from building on the MCP server
I’ve been using the server for something real — pushing structured conversation output into a list, letting a human annotate it there, and reading the annotations back — so these are the edges that turned up in practice rather than from reading the tool list.
create_checklist and create_task both document tags as “Comma-separated list of tags”. They don’t parse it the same way:
| call | tags: "scratch" |
tags: "#scratch" |
|---|---|---|
create_task |
✓ tag scratch |
✓ tag scratch |
create_checklist |
silently dropped | ✓ tag scratch |
So a list created with tags: "scratch" comes back with no tags and no error — the call reports success. Two things would fix it: accept both forms on create_checklist as create_task already does, and say in the description which form is expected. Right now the only way to discover the # requirement is to notice the tags are missing afterwards and guess.
Related: update_checklist has no tags parameter at all, so a list’s tags can only ever be set at creation. Less urgent now that creation works, but it means a mistagged list can’t be corrected through the API.
import_tasks takes the list name from the OPML’s <head><title>. I created a list called cvt-experiment-2026-07-25, imported an OPML whose head title was cvt experiment, and the list was renamed to cvt experiment. Nothing in the tool description mentions this.
It’s a sharp edge in the flow you yourself recommend — “create an empty list with create_checklist, then import OPML into it” — because an OPML exported from another list, or from another tool, carries that list’s title and will quietly rename the destination. An agent doing this on a user’s real list renames something the user never asked to touch. Either ignore <head><title> on import into an existing list, or document that it wins.
Newlines in OPML attributes. survives the round trip intact, including leading indentation. A literal newline in the attribute is normalised to a space — correct XML behaviour, but since OPML is your recommended transport for anything structured, it’s worth stating outright: multi-line item text must use . I lost a test to this before checking.
Plain-text import versus code. With indented text, leading whitespace becomes hierarchy — so pasting a Python function in produces a tree of its own indentation, one item per line. That’s inherent to the format and not a bug, but agents will paste code, so a line in the import_tasks description saying “use OPML for code or any content whose line structure matters” would prevent it. parse_tasks: false correctly protected # and ! in the code from becoming tags and priorities — that part works well and is worth mentioning in the same breath.
Note representation in OPML. Notes come back as a child <outline type="note" isComment="true" text="…" note_id="…" user="…"> rather than an attribute on the parent. Perfectly workable, and discoverable by exporting a list that has one, but undocumented — and it’s the thing you need if you’re diffing an exported tree.
The OPML round trip is genuinely lossless. Structure, notes, priorities, open/closed status and multi-line text all survive export → import unchanged, which is what makes “push a document, let a human mark it up, read the markup back” viable at all. That’s the capability I’m actually building on.
Multi-line item text working at all was a pleasant surprise — I’d assumed items were single-line and designed around it, wrongly. Markdown rendering inside items is what makes it pay off: a fenced code block renders as code, and a whole markdown table in a single item renders as a table. Worth knowing that the same table split across sibling items does not render — so “one structure, one item” is the rule, which is not obvious up front and might deserve a sentence somewhere.
Hello @Charles_Heckscher ,
Which LLM agent are you connecting? Please tell us, and we’ll try to prepare instructions for this.
Thanks,
KIR
Hello @Ralf ,
Thanks for trying this and for the report! There are some more and some less important things in it - but it is definitely useful.
We’ll see what can be fixed ![]()
Anyway, I hope what we have now is already useful.
Kind regards and thanks again,
KIR
Hello @maxkir,
It’s definitely useful; I never meant to question that. Thanks again for working on that. I’m providing this feedback because a lot of work went into it—mostly on the agent’s part—and that effort could be spared for others (maybe by just updating the documentation). Here’s another update from the agent.
Follow-up: a retraction, and what the bug actually is
I need to withdraw the tag finding from my previous post before it costs anyone time. I reported that create_checklist and create_task parse the tags parameter differently — that create_checklist silently drops a plain scratch and requires #scratch. That is not true. There is no tag-parsing bug.
On a freshly created list, every form works:
tags argument |
resulting tags |
|---|---|
"scratch" |
scratch |
"#scratch" |
scratch |
"#claude" |
claude |
"#claude,#session" |
claude,session |
Plain, prefixed, single, comma-separated — all correct, no silent failures. My mistake was drawing the conclusion from lists that had something else done to them, which brings me to the real finding.
import_tasks overwrites the list’s metadata from the OPML <head>This is the same bug as the rename I reported last time, one layer deeper than I understood it. The import doesn’t just take the name from <head><title> — it replaces list-level metadata generally. <head> can express a title, so the list gets renamed. It cannot express tags, so the tags are wiped.
Minimal reproduction:
create_checklist(name: "cvt-tag-a-scratch", tags: "#scratch") → tags: scratch ✓import_tasks(checklist_id, import_content: <one-item OPML whose head title matches the list name>, parse_tasks: false)get_checklist(checklist_id) → tags: (none)One harmless item imported, tags gone, nothing in the response indicating it. Every confusing data point I had fits this without exception: the lists whose tags “failed” were all lists I later imported into; the lists I never imported into kept their tags to this day.
This matters more than the rename, for two reasons. A rename is visible — the user sees the list title change and can put it back. Losing tags is invisible unless you go looking, and lists are often organised by tag, so the list quietly falls out of every view that selects on it. And second: combined with update_checklist having no tags parameter, there is no API route to restore them. Tags can only be set at creation, and creation has already happened. The data is simply unrecoverable through the API.
Suggested fix, the same one as for the rename: on import into an existing list, leave list-level metadata alone. The user asked to import items, not to redefine the list. If the head must win, then update_checklist needs a tags parameter so the damage is at least repairable.
parse_tasks applies to OPML import, and it edits the textThe parameter is documented as “Parse smart syntax (^due, #tags, !priority); mainly for text import (default true)”. “Mainly for text import” reads as though OPML is exempt. It isn’t. Importing this with the default:
<outline text="P3 text containing #hashtag and !bang and ^tomorrow"/>
produced an item whose text had been rewritten to P3 text containing #hashtag and !bang and — the ^tomorrow deleted — plus due="2026-07-26" and tags="hashtag". The identical document with parse_tasks: false came through untouched.
The smart syntax itself is documented on your help pages and I’m not arguing with it. What isn’t documented is that an import applies it, and that matters more for OPML than for plain text, because OPML is what a program uses to move content it did not author. A code sample containing #, prose containing ^, a shell command with ! — all silently altered, with nothing in the response indicating it happened.
Suggestion: default parse_tasks to false when the payload is OPML. OPML has its own attributes for due dates, tags and priority and doesn’t need the shorthand. Failing that, drop “mainly” and say plainly that it applies to both.
An exported item looks like this:
<outline text="This item is collapsed" status="open" _status="indeterminate"/>
<outline text="P4 will be tagged via the API" status="open" tags="question"/>
Text, status, tags, priority, due — everything except identity. Notes do carry note_id, which makes the omission look incidental rather than deliberate.
The consequence is that OPML cannot be the basis for a diff or a sync. To detect what a user changed you have to match items by text, and any two items sharing text become indistinguishable. I hit exactly that: two notes on one item, two identical sibling items, and an edited parent whose children were then re-keyed — all silent losses of the user’s input. I abandoned the export and moved the whole read path to get_tasks, whose JSON carries id and parent_id.
That works, but it splits the two directions across two formats — OPML in, JSON out — for no reason other than the missing attribute. And it sits oddly with the export’s own description, which recommends it as “full-fidelity” and “the PREFERRED format for transferring a whole list between lists or accounts”. True of the content; but a transfer that cannot preserve identity cannot be resumed, reconciled or diffed.
Suggestion: add id to exported outlines, as note_id already is for notes.
collapsed is always falseget_tasks returns a collapsed field on every item and it never reflects reality. Controlled test — two structurally identical subtrees, one collapsed in the web UI and one not:
[74306796] collapsed=False 'This item is not collapsed' children=['1','2','3']
[74306800] collapsed=False 'This item is collapsed' children=['1','2','3']
No item anywhere in that list reports true, and the OPML export has no collapse concept at all, so both representations lose it.
I’m not asking for collapse state — it’s a viewing preference and a client is right to ignore it. But a field that is always false is worse than an absent one, because it looks like an answer. Either populate it or drop it from the payload.
import_tasks returns the created items in document order, matching the order they appear in the OPML. I’ve relied on that across four imports of 24–31 items, pairing the returned ids with the content I sent so I can keep a local baseline without a second round trip. It has held every time, and the ids were even contiguous.
Is that a guarantee I can depend on, or an implementation detail that might change? If it’s intended, one sentence in the tool description would make it safe to build on — and it would take much of the pressure off the missing-id problem above, since it is currently the only way to learn which id belongs to which pushed item.
checkvist_get_tasks returned 403 {"message":"The list doesn't exist or is not available to you"} for a list that had just been deleted. Reasonable, but “doesn’t exist” and “not available to you” are quite different situations for a client deciding whether to recreate a list or stop and ask.
Best,
Ralf
One more post because there may be a data protection risk. Here is the agent’s text:
create_checklist makes the list public — and update_checklist cannot undo it
Found while building a client that pushes conversation content into a list, so the visibility of that list is not cosmetic for us.
Measured against the live account today:
| call | resulting list |
|---|---|
checkvist_create_checklist(name, public: false) — MCP |
public |
checkvist_update_checklist(id, public: false) — MCP |
reports success, stays public |
POST /checklists.json, no public parameter — REST |
private |
POST /checklists.json with checklist[public]=false — REST |
private |
PUT /checklists/<id>.json with checklist[public]=false — REST |
becomes private |
So this is not “the parameter is ignored”. Checkvist’s own default is private, and going through the MCP tool inverts it: a client that explicitly asks for a private list gets a world-readable one. update_checklist then cannot repair it — public: false returns a success message and nothing changes — so the only fix available to a client is to leave the MCP surface and call REST.
What made it hard to notice: the create tool’s output is Created list <name> (id: …) and mentions visibility nowhere. We found it only because our client re-reads the list with get_checklist before every write, and that tool does report public correctly. Between those two calls sat a day of pushing content into lists we believed were private.
Two suggestions, the second more important than the first:
checklist[public]), and treat an absent parameter as “leave it alone” rather than as a default.Created list X (id: …) is friendly but unverifiable. Had it said public: true, this would have been caught in the first minute instead of after a day.This is the second case in the preview where a write tool’s behaviour differs from what it declares — the first being import_tasks annotated destructive: false while replace_existing: true empties the list. Both share a shape: a careful client cannot detect them without a second, independent read.
P.S., relevant to anyone building on import_tasks: an import that times out still commits. We aborted a POST /import.json after 154 ms (curl -m 0.15, exit 28) and the items were on the list regardless. Since the import is not idempotent, an MCP client that treats a timeout as failure and retries will duplicate the entire content. It would help to say so in the tool description.
It’s quickly becoming my favorite way to add stuff to Checkvist from my phone. I’m using it with Claude
Hi,
We fixed the most serious issues from @Ralf report, and I hope now the MCP is more solid and predictable. When needed, OpenAPI spec has been fixed/extended, too.
collapsed no longer reported when meaninglessTo validate the change, I believe it is required to remove/add MCP back.
@Ralf thanks a lot for the testing!
Best,
KIR
Hi folks,
While testing, we tried to create tasks/lists using the voice interface, via LLM+MCP
But looks like MCP servers are isolated from the voice interface both for OpenAI and Claude.
At least for write operations.
Have you tried accessing Checkvist with voice somehow? Any luck with that?
Thanks,
KIR