Add get-task singular, move-task tool, and surface real Graph errors#9
Open
rathga wants to merge 7 commits into
Open
Add get-task singular, move-task tool, and surface real Graph errors#9rathga wants to merge 7 commits into
rathga wants to merge 7 commits into
Conversation
- 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 JSONget-taskstruncatesbody.contentto 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-taskreturns 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$selectmakeGraphRequestswallowed non-2xx responses, so Graph 400/404 bodies only reached stderr and MCP callers saw a generic"Failed to retrieve X". Now a module-scopedlastGraphErrorcaptures the full HTTP status + response body;get-tasksandget-taskpull it into their user-visible response.normalizeSelect()also trims whitespace per field and auto-includesid(Graph 400s if$selectomits it).Related:
makeGraphRequestnow handles 204 No Content cleanly so DELETE/empty-body PATCH responses don't pollutelastGraphErrorwith spurious JSON-parse errors.3.
feat(move-task)— atomic cross-list movesMS 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-taskdoes it in one call: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-taskeffectively "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 exposeorderHintonmicrosoft.graph.todoTaskin either v1.0 or beta. ProbingGET /me/todo/lists/{id}/tasks?$select=orderHintreturns:A PATCH with
orderHintreturns 200 but silently ignores the field. Arbitrary task reordering is not achievable via the public Graph API.orderHintexists onplannerTask(different product, Planner) — not ontodoTask.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.contentNOT truncated viaget-task(206 chars round-tripped intact)Verification
npm run buildcleanget-task, +move-task)Commits
get-tasksingular returning full task JSON$selectwhitespace normalization + auto-idmakeGraphRequesthandles 204/empty-body without pollutinglastGraphErrormove-tasktool for full-fidelity within/cross-list movesorderHintparam (not supported by Graph on todoTask)