Skip to content

Add get-task singular, move-task tool, and surface real Graph errors#9

Open
rathga wants to merge 7 commits into
jhirono:mainfrom
rathga:fix/reorder-and-body-truncation
Open

Add get-task singular, move-task tool, and surface real Graph errors#9
rathga wants to merge 7 commits into
jhirono:mainfrom
rathga:fix/reorder-and-body-truncation

Conversation

@rathga

@rathga rathga commented Apr 22, 2026

Copy link
Copy Markdown

Tools to unblock common MS Todo workflows via MCP — safe cross-list moves, full-fidelity task reads, and proper Graph error visibility. Smoke-tested end-to-end against live Graph.

What changed

1. feat(get-task) — singular lookup returning full task JSON
get-tasks truncates body.content to a 50-char preview, causing silent data loss when a caller tries to round-trip a task (read → recreate → delete). Rather than inflating every list row, get-task returns the raw todoTask JSON (full body, dates, etc.) so list views stay lean with full detail on demand.

2. fix(graph) — surface real Graph errors and harden $select
makeGraphRequest swallowed non-2xx responses, so Graph 400/404 bodies only reached stderr and MCP callers saw a generic "Failed to retrieve X". Now a module-scoped lastGraphError captures the full HTTP status + response body; get-tasks and get-task pull it into their user-visible response. normalizeSelect() also trims whitespace per field and auto-includes id (Graph 400s if $select omits it).

Related: makeGraphRequest now handles 204 No Content cleanly so DELETE/empty-body PATCH responses don't pollute lastGraphError with spurious JSON-parse errors.

3. feat(move-task) — atomic cross-list moves
MS Graph does not allow changing a todoTask's parentFolderId, so cross-list moves require create-in-target + delete-from-source. Doing that client-side is multiple tool calls and prone to partial failure. move-task does it in one call:

  1. GET source with full fields
  2. GET source's checklist items (subtasks)
  3. POST copy into target list with all writable fields (title, body, dueDateTime, startDateTime, reminderDateTime, isReminderOn, completedDateTime, importance, status, categories, recurrence)
  4. POST each checklist item into the new task
  5. DELETE source — only after create succeeded

Fail-safe: if the target-list create fails, the source is untouched. If the source delete fails after a successful create, the new task is preserved and the response warns with the orphaned source task ID for manual cleanup. No silent duplicates, no silent data loss.

With sourceListId == targetListId, move-task effectively "bumps the task to the top of its list" (new tasks land at the top by default in MS Todo). This covers the most common prioritization workflow.

What's NOT included (and why)

orderHint: MS Graph does not expose orderHint on microsoft.graph.todoTask in either v1.0 or beta. Probing GET /me/todo/lists/{id}/tasks?$select=orderHint returns:

BadRequest: Could not find a property named 'orderHint' on type 'microsoft.graph.todoTask'.

A PATCH with orderHint returns 200 but silently ignores the field. Arbitrary task reordering is not achievable via the public Graph API. orderHint exists on plannerTask (different product, Planner) — not on todoTask.

Smoke test (live Graph)

Moved a disposable test task with rich body (206 chars), high importance, multiple categories, and a due date — between two real lists, round-tripped there and back, verifying every hop. 5/5 field-preservation checks pass:

  • body.content NOT truncated via get-task (206 chars round-tripped intact)
  • Within-list move preserves all fields
  • Cross-list move preserves all fields
  • Source task fully deleted after each move (404 on old ID)
  • Full round-trip (A → B → A) preserves all fields

Verification

  • npm run build clean
  • Stdio smoke test confirms tool count is 13 → 15 (+get-task, +move-task)

Commits

  1. get-task singular returning full task JSON
  2. Graph error surfacing + $select whitespace normalization + auto-id
  3. makeGraphRequest handles 204/empty-body without polluting lastGraphError
  4. move-task tool for full-fidelity within/cross-list moves
  5. Revert dead orderHint param (not supported by Graph on todoTask)

rathga and others added 7 commits March 4, 2026 20:07
- Fix dotenv to resolve .env relative to script location instead of cwd,
  which may differ when launched by Claude Desktop
- Add automatic token refresh on expiration and 401 retry logic
- Use TENANT_ID from env for token refresh endpoint instead of hardcoding
- Resolve tokens.json relative to script location with cwd fallback
- Pass token expiration time through CLI to server for proactive refresh
- Add Claude Desktop configuration instructions and troubleshooting to README
- Remove verbose request/response logging

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
MS Graph PATCH /me/todo/lists/{listId}/tasks/{taskId} accepts
orderHint on the todoTask resource, but the MCP schema omitted it —
so there was no way to reorder tasks via the MCP. This adds an
optional orderHint string param on update-task that is forwarded to
the PATCH body when provided, matching MS Graph semantics (empty
string resets to default; non-empty values sort reverse-
lexicographic descending).

Ref: https://learn.microsoft.com/graph/api/resources/todotask
get-tasks truncates body.content to a 50-char preview, which causes
silent data loss when a caller round-trips a task through delete+
recreate as a reorder workaround. Rather than inflating every
get-tasks row, add a new get-task (singular) tool that returns the
raw todoTask JSON (including full body.content, orderHint,
reminder/start/completed dates, and other fields the list summary
omits).

The Task interface is extended with the additional fields that
get-task may surface (orderHint, createdDateTime,
lastModifiedDateTime, etc.) so downstream formatting can rely on
them.
get-tasks was returning a generic "Failed to retrieve tasks" on any
non-2xx Graph response because makeGraphRequest swallows errors and
returns null. The real HTTP status + response body only reached
stderr, which MCP clients don't surface — so a 400 from a bad
\$select looked identical to auth loss, and passing select=<any
field> appeared to fail unconditionally.

Changes:

- makeGraphRequest now records the last error (HTTP status + body,
  or thrown-error message) into a module-scoped lastGraphError;
  callers can pull it into user-visible output via
  consumeLastGraphError(). Wired into get-tasks and get-task so the
  MCP response now contains the actual Graph error detail on
  failure.
- New normalizeSelect() helper: trims whitespace per field, drops
  empties, and auto-includes \`id\`. Graph returns an opaque 400 on
  todoTask \$select that omits id — auto-including it matches user
  intent and removes a common footgun.
makeGraphRequest previously called response.json() unconditionally
on successful responses. For DELETE (and PATCH with no Prefer:
return=representation header) MS Graph returns 204 with an empty
body, and the parser threw — the outer catch then populated
lastGraphError with a misleading 'Unexpected end of JSON input'.

Now: return null for 204, return null for empty text, and only
JSON.parse non-empty bodies. lastGraphError stays unset on
successful empty-body responses so callers can reliably distinguish
success from failure via consumeLastGraphError().
MS Graph does not allow changing a todoTask's parent list via PATCH,
so cross-list moves and same-list reorders-from-scratch both require
create-in-target + delete-source. Doing that client-side needs
multiple tool calls per move and is error-prone — a partial failure
can leave a silent duplicate or lose the source.

The new move-task tool performs the full round-trip in one call:

1. GET the source task with full fields
2. GET its checklist items (subtasks)
3. POST a copy to the target list, carrying over title, body,
   dueDateTime, startDateTime, reminderDateTime, isReminderOn,
   completedDateTime, importance, status, categories, recurrence,
   and orderHint (caller-supplied orderHint wins; otherwise the
   source orderHint is preserved)
4. POST each checklist item into the new task
5. DELETE the source — but only after the create succeeded

Error handling is fail-safe: if the target-list create fails, the
source is left untouched and the caller is told so explicitly. If
the source delete fails after a successful create, the new task is
preserved and the response warns with the source task ID so the
caller can clean up manually — no silent data loss.
Empirical testing against both https://graph.microsoft.com/v1.0 and
https://graph.microsoft.com/beta returns:

  BadRequest: Could not find a property named 'orderHint' on type
  'microsoft.graph.todoTask'.

orderHint is exposed on microsoft.graph.plannerTask (Planner, a
different product) but not on microsoft.graph.todoTask. The Graph
PATCH silently ignored the field (returning 200) which is why the
initial fix looked plausible until smoke-tested end-to-end.

Removed:
- orderHint param from update-task schema + PATCH body
- orderHint param + preservation logic from move-task
- orderHint field from the Task interface

move-task still works for reordering WITHIN a list (sourceListId ==
targetListId) because newly-created MS Todo tasks land at the top
of the target list by default — so 'move in place' is effectively
'bump to top of list', which covers the most common
prioritization workflow. Arbitrary reorder is not achievable via
the public Graph API.
@rathga rathga changed the title Fix task reorder: add orderHint, get-task singular, surface Graph errors Add get-task singular, move-task tool, and surface real Graph errors Apr 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant