Initial commit: Microsoft To Do for Claude (skill + .mcpb extension)
Claude Code skill (CLI over Microsoft Graph, delegated auth-code + PKCE) and a Claude Desktop .mcpb extension sharing one dependency-free core client. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
9
mcp-extension/.gitignore
vendored
Normal file
9
mcp-extension/.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
# Generated / bundled artifacts
|
||||
node_modules/
|
||||
# synced from the skill by scripts/sync-core.mjs
|
||||
server/lib/graph.mjs
|
||||
*.mcpb
|
||||
|
||||
# Signing material — NEVER commit the private key
|
||||
key.pem
|
||||
cert.pem
|
||||
109
mcp-extension/README.md
Normal file
109
mcp-extension/README.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# Microsoft To Do — Claude Desktop extension (.mcpb)
|
||||
|
||||
A one-click Claude Desktop extension that connects Claude to **Microsoft To Do**.
|
||||
It wraps the same core Graph client as the [`ms-todo` skill](../.claude/skills/ms-todo)
|
||||
(`MSTodoClient`) and exposes it as MCP tools over stdio. Each user signs in as
|
||||
**themselves** (delegated, browser auth-code + PKCE); their refresh token is
|
||||
cached privately on their own machine.
|
||||
|
||||
The built artifact is **`ms-todo.mcpb`**.
|
||||
|
||||
---
|
||||
|
||||
## For end users — install in Claude Desktop
|
||||
|
||||
1. Get `ms-todo.mcpb` from your admin.
|
||||
2. Open **Claude Desktop → Settings → Extensions**.
|
||||
3. Drag `ms-todo.mcpb` in (or click **Install extension** and select it).
|
||||
4. Optionally set your **Time zone** in the extension's settings (used for due /
|
||||
start / reminder dates; default UTC). Leave **Read-only mode** off for full
|
||||
access.
|
||||
5. In a chat, say: **"Sign in to Microsoft To Do."** Claude runs the `todo_login`
|
||||
tool, your browser opens to the Microsoft sign-in page, you approve, and you're
|
||||
connected. (You only do this once.)
|
||||
6. Try it: *"What's on my To Do list?"*, *"Add 'call dentist' due Friday to Tasks."*
|
||||
|
||||
> If Claude Desktop warns that the extension is **not verified/signed**, that's
|
||||
> expected for an internally distributed extension — install it anyway (or have
|
||||
> your admin sign it, see below).
|
||||
|
||||
### What it can do (tools)
|
||||
|
||||
`todo_login`, `todo_logout`, `todo_test`, `todo_list_lists`, `todo_list_tasks`,
|
||||
`todo_get_task`, `todo_create_task`, `todo_update_task`, `todo_complete_task`,
|
||||
`todo_delete_task`, `todo_create_list`, `todo_update_list`, `todo_delete_list`,
|
||||
`todo_list_checklist`, `todo_add_checklist_item`, `todo_check_checklist_item`,
|
||||
`todo_delete_checklist_item`.
|
||||
|
||||
---
|
||||
|
||||
## For the admin — one-time Entra setup (org-wide)
|
||||
|
||||
The extension ships with a **baked-in** public-client app id and tenant id (in
|
||||
`manifest.json` → `server.mcp_config.env`). There is **no client secret** — these
|
||||
identifiers are not secrets. For smooth org-wide use, in
|
||||
[entra.microsoft.com](https://entra.microsoft.com) on that app registration:
|
||||
|
||||
1. **Authentication → Mobile and desktop applications** — ensure redirect URIs
|
||||
`http://localhost` **and** `http://127.0.0.1` are present (the extension uses a
|
||||
loopback redirect during sign-in). *Already configured for the current app.*
|
||||
2. **API permissions** — delegated `Tasks.ReadWrite`, `offline_access`,
|
||||
`User.Read`. Click **Grant admin consent for <tenant>** so individual
|
||||
users are **not** prompted to consent (and so it works even if user consent is
|
||||
disabled in your tenant).
|
||||
3. The tenant id is baked single-tenant, so only your org's accounts can sign in.
|
||||
To support guests/other tenants, change the app to multi-tenant and set
|
||||
`TODO_TENANT_ID` to `organizations` (then rebuild).
|
||||
|
||||
To change the baked id/tenant, edit `manifest.json` and rebuild (below).
|
||||
|
||||
---
|
||||
|
||||
## Rebuilding the .mcpb
|
||||
|
||||
Requires Node 18+. From this directory:
|
||||
|
||||
```bash
|
||||
npm run pack
|
||||
```
|
||||
|
||||
That runs three steps: `sync` (copy the canonical `graph.mjs` from the skill into
|
||||
`server/lib/`), `npm ci --omit=dev` (install runtime deps to bundle), and
|
||||
`mcpb pack` (zip into `ms-todo.mcpb`). Or run them manually:
|
||||
|
||||
```bash
|
||||
node scripts/sync-core.mjs
|
||||
npm ci --omit=dev
|
||||
npx -y @anthropic-ai/mcpb pack . ms-todo.mcpb
|
||||
```
|
||||
|
||||
Useful checks:
|
||||
|
||||
```bash
|
||||
npx -y @anthropic-ai/mcpb validate manifest.json
|
||||
npx -y @anthropic-ai/mcpb info ms-todo.mcpb
|
||||
```
|
||||
|
||||
### Optional: sign the extension
|
||||
|
||||
Unsigned extensions install with a "not verified" warning. You can self-sign so
|
||||
the warning shows your identity (full removal of the warning needs a CA-issued
|
||||
cert your org trusts):
|
||||
|
||||
```bash
|
||||
npx -y @anthropic-ai/mcpb sign ms-todo.mcpb --cert cert.pem --key key.pem
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## How it differs from the Claude Code skill
|
||||
|
||||
| | `ms-todo` skill | this `.mcpb` extension |
|
||||
|---|---|---|
|
||||
| Host | Claude Code (CLI/IDE) on your machine | Claude Desktop |
|
||||
| Surface | `node todo.mjs …` commands | MCP tools (`todo_*`) |
|
||||
| Sign-in | `node todo.mjs login` | `todo_login` tool ("sign in to Microsoft To Do") |
|
||||
| Core client | `scripts/lib/graph.mjs` | the **same** file, synced into `server/lib/` |
|
||||
|
||||
The Graph + auth logic lives in one place; this bundle is just a thin MCP wrapper
|
||||
plus a manifest. Token cache defaults to `~/.ms-todo/token-cache.json` per user.
|
||||
69
mcp-extension/manifest.json
Normal file
69
mcp-extension/manifest.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"manifest_version": "0.3",
|
||||
"name": "ms-todo",
|
||||
"display_name": "Microsoft To Do",
|
||||
"version": "1.0.0",
|
||||
"description": "Read and write your Microsoft To Do tasks, lists, and subtasks from Claude.",
|
||||
"long_description": "Connects Claude to Microsoft To Do via the Microsoft Graph API. You sign in once as yourself (a browser window opens); the connection then reads and writes your task lists, tasks, and checklist items. Each user's sign-in is private to their own machine.",
|
||||
"author": {
|
||||
"name": "PESCO Inc."
|
||||
},
|
||||
"keywords": ["microsoft", "to do", "todo", "tasks", "graph", "productivity"],
|
||||
"license": "MIT",
|
||||
"compatibility": {
|
||||
"platforms": ["darwin", "win32", "linux"],
|
||||
"runtimes": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
"type": "node",
|
||||
"entry_point": "server/index.mjs",
|
||||
"mcp_config": {
|
||||
"command": "node",
|
||||
"args": ["${__dirname}/server/index.mjs"],
|
||||
"env": {
|
||||
"TODO_CLIENT_ID": "9af4a8a3-5290-4089-9058-a29dcce63c4f",
|
||||
"TODO_TENANT_ID": "94c6c62d-8fe7-416f-8fef-7d8620b95819",
|
||||
"TODO_TOKEN_CACHE": "${HOME}/.ms-todo/token-cache.json",
|
||||
"TODO_TIMEZONE": "${user_config.timezone}",
|
||||
"TODO_READONLY": "${user_config.read_only}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tools": [
|
||||
{ "name": "todo_login", "description": "Sign in (opens your browser)" },
|
||||
{ "name": "todo_logout", "description": "Forget the cached sign-in" },
|
||||
{ "name": "todo_test", "description": "Verify connectivity and show your lists" },
|
||||
{ "name": "todo_list_lists", "description": "List your task lists" },
|
||||
{ "name": "todo_list_tasks", "description": "Query tasks in a list" },
|
||||
{ "name": "todo_get_task", "description": "Get one task" },
|
||||
{ "name": "todo_create_task", "description": "Create a task" },
|
||||
{ "name": "todo_update_task", "description": "Update a task" },
|
||||
{ "name": "todo_complete_task", "description": "Mark a task complete" },
|
||||
{ "name": "todo_delete_task", "description": "Delete a task" },
|
||||
{ "name": "todo_create_list", "description": "Create a task list" },
|
||||
{ "name": "todo_update_list", "description": "Rename a task list" },
|
||||
{ "name": "todo_delete_list", "description": "Delete a task list" },
|
||||
{ "name": "todo_list_checklist", "description": "List checklist items (subtasks)" },
|
||||
{ "name": "todo_add_checklist_item", "description": "Add a checklist item" },
|
||||
{ "name": "todo_check_checklist_item", "description": "Check/uncheck a checklist item" },
|
||||
{ "name": "todo_delete_checklist_item", "description": "Delete a checklist item" }
|
||||
],
|
||||
"user_config": {
|
||||
"timezone": {
|
||||
"type": "string",
|
||||
"title": "Time zone",
|
||||
"description": "Time zone for due/start/reminder dates (IANA like America/Denver, or a Windows zone name). Default UTC.",
|
||||
"default": "UTC",
|
||||
"required": false
|
||||
},
|
||||
"read_only": {
|
||||
"type": "boolean",
|
||||
"title": "Read-only mode",
|
||||
"description": "If enabled, only viewing tools are available (no create/update/delete).",
|
||||
"default": false,
|
||||
"required": false
|
||||
}
|
||||
}
|
||||
}
|
||||
1169
mcp-extension/package-lock.json
generated
Normal file
1169
mcp-extension/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
16
mcp-extension/package.json
Normal file
16
mcp-extension/package.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "ms-todo-mcp-extension",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Microsoft To Do MCP server, packaged as a Claude Desktop .mcpb extension",
|
||||
"scripts": {
|
||||
"sync": "node scripts/sync-core.mjs",
|
||||
"start": "node server/index.mjs",
|
||||
"pack": "npm run sync && npm ci --omit=dev && npx -y @anthropic-ai/mcpb pack . ms-todo.mcpb"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.12.0",
|
||||
"zod": "^3.23.8"
|
||||
}
|
||||
}
|
||||
16
mcp-extension/scripts/sync-core.mjs
Normal file
16
mcp-extension/scripts/sync-core.mjs
Normal file
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env node
|
||||
// Copy the canonical core client into the extension so the bundle is
|
||||
// self-contained. The single source of truth lives in the skill; this keeps the
|
||||
// extension's copy (server/lib/graph.mjs) in sync. Run before packing.
|
||||
|
||||
import { copyFileSync, mkdirSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const src = join(here, "..", "..", ".claude", "skills", "ms-todo", "scripts", "lib", "graph.mjs");
|
||||
const dest = join(here, "..", "server", "lib", "graph.mjs");
|
||||
|
||||
mkdirSync(dirname(dest), { recursive: true });
|
||||
copyFileSync(src, dest);
|
||||
console.log(`Synced core client:\n ${src}\n -> ${dest}`);
|
||||
443
mcp-extension/server/index.mjs
Normal file
443
mcp-extension/server/index.mjs
Normal file
@@ -0,0 +1,443 @@
|
||||
#!/usr/bin/env node
|
||||
// Microsoft To Do MCP server (stdio) — packaged as a Claude Desktop .mcpb extension.
|
||||
//
|
||||
// Wraps the SAME core client the skill uses (lib/graph.mjs) and exposes it as MCP
|
||||
// tools. Auth is DELEGATED (browser auth-code + PKCE): each user signs in as
|
||||
// themselves via the `todo_login` tool, which opens their browser. The refresh
|
||||
// token is cached per-user on disk so later sessions refresh silently.
|
||||
//
|
||||
// Config comes from the environment (the .mcpb manifest sets these):
|
||||
// TODO_CLIENT_ID (required) public-client app id — baked into the manifest
|
||||
// TODO_TENANT_ID (optional) tenant id / "common" — baked into the manifest
|
||||
// TODO_TOKEN_CACHE (optional) path to the per-user token cache file
|
||||
// TODO_TIMEZONE (optional) zone for due/start/reminder dates (default UTC)
|
||||
// TODO_READONLY (optional) "true"/"1" -> register only read tools
|
||||
|
||||
import { homedir } from "node:os";
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { z } from "zod";
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
// Single source of truth — synced from the skill's scripts/lib/graph.mjs.
|
||||
import { MSTodoClient, GraphError, AuthRequiredError, toDateTimeTimeZone } from "./lib/graph.mjs";
|
||||
|
||||
const READONLY = /^(1|true|yes)$/i.test(process.env.TODO_READONLY ?? "");
|
||||
const TIMEZONE = process.env.TODO_TIMEZONE || "UTC";
|
||||
|
||||
const tokenCachePath = process.env.TODO_TOKEN_CACHE || join(homedir(), ".ms-todo", "token-cache.json");
|
||||
try {
|
||||
mkdirSync(dirname(tokenCachePath), { recursive: true });
|
||||
} catch {
|
||||
/* best-effort; the client will surface a clear error if it can't write */
|
||||
}
|
||||
|
||||
const client = new MSTodoClient({
|
||||
clientId: process.env.TODO_CLIENT_ID,
|
||||
tenantId: process.env.TODO_TENANT_ID || "common",
|
||||
tokenCachePath,
|
||||
});
|
||||
|
||||
// ---- helpers --------------------------------------------------------------
|
||||
|
||||
const ok = (data) => ({
|
||||
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
||||
});
|
||||
const fail = (err) => ({
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text:
|
||||
err instanceof AuthRequiredError
|
||||
? `${err.message} (use the "todo_login" tool to sign in)`
|
||||
: err instanceof GraphError
|
||||
? `Graph error (${err.status}${err.code ? " " + err.code : ""}): ${err.message}`
|
||||
: `Error: ${err.message}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
});
|
||||
|
||||
/** Build a todoTask body from friendly params + an optional raw fields object. */
|
||||
function buildTaskFields(
|
||||
{ title, body, importance, status, due, start, reminder, categories, extraFields },
|
||||
requireTitle,
|
||||
) {
|
||||
const fields = {};
|
||||
if (title !== undefined) fields.title = title;
|
||||
if (importance !== undefined) fields.importance = importance;
|
||||
if (status !== undefined) fields.status = status;
|
||||
if (body !== undefined) fields.body = { content: body, contentType: "text" };
|
||||
if (due !== undefined) fields.dueDateTime = toDateTimeTimeZone(due, TIMEZONE);
|
||||
if (start !== undefined) fields.startDateTime = toDateTimeTimeZone(start, TIMEZONE);
|
||||
if (reminder !== undefined) {
|
||||
fields.reminderDateTime = toDateTimeTimeZone(reminder, TIMEZONE);
|
||||
fields.isReminderOn = true;
|
||||
}
|
||||
if (categories !== undefined) fields.categories = categories;
|
||||
if (extraFields && typeof extraFields === "object") Object.assign(fields, extraFields);
|
||||
if (requireTitle && !fields.title) throw new Error("A task `title` is required.");
|
||||
return fields;
|
||||
}
|
||||
|
||||
// Shared schema fragments
|
||||
const LIST = z.string().describe("Task list display name (e.g. \"Tasks\") or its opaque list id.");
|
||||
const TASK_ID = z.string().describe("The task id (from todo_list_tasks / todo_get_task).");
|
||||
const IMPORTANCE = z.enum(["low", "normal", "high"]).optional();
|
||||
const STATUS = z
|
||||
.enum(["notStarted", "inProgress", "completed", "waitingOnOthers", "deferred"])
|
||||
.optional();
|
||||
const DATEISH = z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Date like "2026-07-01" or "2026-07-01T17:00:00" (uses the configured time zone).');
|
||||
const TASK_WRITE_FIELDS = {
|
||||
title: z.string().optional().describe("Task title."),
|
||||
body: z.string().optional().describe("Note/body text."),
|
||||
importance: IMPORTANCE,
|
||||
status: STATUS,
|
||||
due: DATEISH,
|
||||
start: DATEISH,
|
||||
reminder: DATEISH.describe("Reminder date/time; also turns the reminder on."),
|
||||
categories: z.array(z.string()).optional().describe("Category names (must exist in the user's Outlook categories)."),
|
||||
extraFields: z
|
||||
.record(z.any())
|
||||
.optional()
|
||||
.describe("Raw todoTask properties merged last, for anything not covered above (e.g. recurrence)."),
|
||||
};
|
||||
|
||||
// ---- server ---------------------------------------------------------------
|
||||
|
||||
const server = new McpServer({ name: "ms-todo", version: "1.0.0" });
|
||||
|
||||
// --- auth ---
|
||||
server.registerTool(
|
||||
"todo_login",
|
||||
{
|
||||
title: "Sign in to Microsoft To Do",
|
||||
description:
|
||||
"Open the system browser to sign in (delegated auth-code + PKCE). Run this once per user; the refresh token is cached and reused. Blocks until sign-in completes.",
|
||||
inputSchema: {},
|
||||
annotations: { readOnlyHint: false, openWorldHint: true },
|
||||
},
|
||||
async () => {
|
||||
let authorizeUrl;
|
||||
try {
|
||||
const account = await client.login((p) => {
|
||||
authorizeUrl = p.authorizeUrl;
|
||||
console.error(`[ms-todo] Opening browser to sign in. If it didn't open: ${p.authorizeUrl}`);
|
||||
});
|
||||
return ok({ ok: true, signedIn: account });
|
||||
} catch (e) {
|
||||
if (authorizeUrl) {
|
||||
return fail(new Error(`${e.message}\nOpen this URL to sign in manually: ${authorizeUrl}`));
|
||||
}
|
||||
return fail(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"todo_logout",
|
||||
{
|
||||
title: "Sign out",
|
||||
description: "Forget the cached sign-in (deletes the local token cache).",
|
||||
inputSchema: {},
|
||||
annotations: { readOnlyHint: false, destructiveHint: true },
|
||||
},
|
||||
async () => {
|
||||
try {
|
||||
return ok({ ok: true, loggedOut: client.logout() });
|
||||
} catch (e) {
|
||||
return fail(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"todo_test",
|
||||
{
|
||||
title: "Test connectivity",
|
||||
description: "Verify the cached sign-in works; returns the signed-in user and their task lists.",
|
||||
inputSchema: {},
|
||||
annotations: { readOnlyHint: true },
|
||||
},
|
||||
async () => {
|
||||
try {
|
||||
return ok(await client.test());
|
||||
} catch (e) {
|
||||
return fail(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// --- task lists ---
|
||||
server.registerTool(
|
||||
"todo_list_lists",
|
||||
{
|
||||
title: "List task lists",
|
||||
description: "Enumerate all of the user's To Do task lists (displayName + id).",
|
||||
inputSchema: {},
|
||||
annotations: { readOnlyHint: true },
|
||||
},
|
||||
async () => {
|
||||
try {
|
||||
return ok(await client.listTaskLists());
|
||||
} catch (e) {
|
||||
return fail(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// --- tasks: read ---
|
||||
server.registerTool(
|
||||
"todo_list_tasks",
|
||||
{
|
||||
title: "Query tasks",
|
||||
description:
|
||||
"List tasks in a list. Filter examples: \"status ne 'completed'\", \"importance eq 'high'\". orderby e.g. \"dueDateTime/dateTime asc\". select is a comma list of properties. Set all=true to fetch every page.",
|
||||
inputSchema: {
|
||||
list: LIST,
|
||||
filter: z.string().optional().describe("OData $filter, e.g. status ne 'completed'"),
|
||||
select: z.string().optional().describe("Comma-separated properties, e.g. title,status,dueDateTime"),
|
||||
orderby: z.string().optional().describe("OData $orderby, e.g. dueDateTime/dateTime asc"),
|
||||
top: z.number().int().positive().optional().describe("Page size / max items"),
|
||||
all: z.boolean().optional().describe("Follow pagination and return all tasks"),
|
||||
},
|
||||
annotations: { readOnlyHint: true },
|
||||
},
|
||||
async ({ list, filter, select, orderby, top, all }) => {
|
||||
try {
|
||||
return ok(await client.listTasks(list, { filter, select, orderby, top, all }));
|
||||
} catch (e) {
|
||||
return fail(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"todo_get_task",
|
||||
{
|
||||
title: "Get one task",
|
||||
description: "Fetch a single task by id.",
|
||||
inputSchema: { list: LIST, taskId: TASK_ID },
|
||||
annotations: { readOnlyHint: true },
|
||||
},
|
||||
async ({ list, taskId }) => {
|
||||
try {
|
||||
return ok(await client.getTask(list, taskId));
|
||||
} catch (e) {
|
||||
return fail(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// --- checklist: read ---
|
||||
server.registerTool(
|
||||
"todo_list_checklist",
|
||||
{
|
||||
title: "List checklist items",
|
||||
description: "List the checklist items (subtasks) on a task.",
|
||||
inputSchema: { list: LIST, taskId: TASK_ID },
|
||||
annotations: { readOnlyHint: true },
|
||||
},
|
||||
async ({ list, taskId }) => {
|
||||
try {
|
||||
return ok(await client.listChecklistItems(list, taskId));
|
||||
} catch (e) {
|
||||
return fail(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// --- write tools (skipped entirely when TODO_READONLY) ---
|
||||
if (!READONLY) {
|
||||
server.registerTool(
|
||||
"todo_create_task",
|
||||
{
|
||||
title: "Create a task",
|
||||
description: "Create a task in a list. `title` is required.",
|
||||
inputSchema: { list: LIST, ...TASK_WRITE_FIELDS },
|
||||
annotations: { readOnlyHint: false, destructiveHint: false },
|
||||
},
|
||||
async ({ list, ...rest }) => {
|
||||
try {
|
||||
return ok(await client.createTask(list, buildTaskFields(rest, true)));
|
||||
} catch (e) {
|
||||
return fail(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"todo_update_task",
|
||||
{
|
||||
title: "Update a task",
|
||||
description: "Update a task (partial — only provided properties change).",
|
||||
inputSchema: { list: LIST, taskId: TASK_ID, ...TASK_WRITE_FIELDS },
|
||||
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
|
||||
},
|
||||
async ({ list, taskId, ...rest }) => {
|
||||
try {
|
||||
return ok(await client.updateTask(list, taskId, buildTaskFields(rest, false)));
|
||||
} catch (e) {
|
||||
return fail(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"todo_complete_task",
|
||||
{
|
||||
title: "Complete a task",
|
||||
description: "Mark a task completed (shorthand for setting status to completed).",
|
||||
inputSchema: { list: LIST, taskId: TASK_ID },
|
||||
annotations: { readOnlyHint: false, idempotentHint: true },
|
||||
},
|
||||
async ({ list, taskId }) => {
|
||||
try {
|
||||
return ok(await client.completeTask(list, taskId));
|
||||
} catch (e) {
|
||||
return fail(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"todo_delete_task",
|
||||
{
|
||||
title: "Delete a task",
|
||||
description: "Delete a task by id. IRREVERSIBLE — confirm the id with the user first.",
|
||||
inputSchema: { list: LIST, taskId: TASK_ID },
|
||||
annotations: { readOnlyHint: false, destructiveHint: true },
|
||||
},
|
||||
async ({ list, taskId }) => {
|
||||
try {
|
||||
await client.deleteTask(list, taskId);
|
||||
return ok({ ok: true, deleted: taskId });
|
||||
} catch (e) {
|
||||
return fail(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"todo_create_list",
|
||||
{
|
||||
title: "Create a task list",
|
||||
description: "Create a new To Do task list.",
|
||||
inputSchema: { name: z.string().describe("Display name for the new list.") },
|
||||
annotations: { readOnlyHint: false, destructiveHint: false },
|
||||
},
|
||||
async ({ name }) => {
|
||||
try {
|
||||
return ok(await client.createTaskList(name));
|
||||
} catch (e) {
|
||||
return fail(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"todo_update_list",
|
||||
{
|
||||
title: "Rename a task list",
|
||||
description: "Rename an existing task list.",
|
||||
inputSchema: { list: LIST, name: z.string().describe("New display name.") },
|
||||
annotations: { readOnlyHint: false, idempotentHint: true },
|
||||
},
|
||||
async ({ list, name }) => {
|
||||
try {
|
||||
return ok(await client.updateTaskList(list, name));
|
||||
} catch (e) {
|
||||
return fail(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"todo_delete_list",
|
||||
{
|
||||
title: "Delete a task list",
|
||||
description: "Delete a task list AND all of its tasks. IRREVERSIBLE — confirm with the user first.",
|
||||
inputSchema: { list: LIST },
|
||||
annotations: { readOnlyHint: false, destructiveHint: true },
|
||||
},
|
||||
async ({ list }) => {
|
||||
try {
|
||||
await client.deleteTaskList(list);
|
||||
return ok({ ok: true, deletedList: list });
|
||||
} catch (e) {
|
||||
return fail(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"todo_add_checklist_item",
|
||||
{
|
||||
title: "Add a checklist item",
|
||||
description: "Add a checklist item (subtask) to a task.",
|
||||
inputSchema: { list: LIST, taskId: TASK_ID, name: z.string().describe("Subtask text.") },
|
||||
annotations: { readOnlyHint: false, destructiveHint: false },
|
||||
},
|
||||
async ({ list, taskId, name }) => {
|
||||
try {
|
||||
return ok(await client.createChecklistItem(list, taskId, name));
|
||||
} catch (e) {
|
||||
return fail(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"todo_check_checklist_item",
|
||||
{
|
||||
title: "Check/uncheck a checklist item",
|
||||
description: "Mark a checklist item checked or unchecked (and optionally rename it).",
|
||||
inputSchema: {
|
||||
list: LIST,
|
||||
taskId: TASK_ID,
|
||||
itemId: z.string().describe("The checklist item id."),
|
||||
checked: z.boolean().optional().describe("true to check (default), false to uncheck."),
|
||||
name: z.string().optional().describe("Optional new text for the item."),
|
||||
},
|
||||
annotations: { readOnlyHint: false, idempotentHint: true },
|
||||
},
|
||||
async ({ list, taskId, itemId, checked, name }) => {
|
||||
try {
|
||||
return ok(
|
||||
await client.updateChecklistItem(list, taskId, itemId, {
|
||||
isChecked: checked ?? true,
|
||||
displayName: name,
|
||||
}),
|
||||
);
|
||||
} catch (e) {
|
||||
return fail(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"todo_delete_checklist_item",
|
||||
{
|
||||
title: "Delete a checklist item",
|
||||
description: "Delete a checklist item (subtask) from a task.",
|
||||
inputSchema: { list: LIST, taskId: TASK_ID, itemId: z.string().describe("The checklist item id.") },
|
||||
annotations: { readOnlyHint: false, destructiveHint: true },
|
||||
},
|
||||
async ({ list, taskId, itemId }) => {
|
||||
try {
|
||||
await client.deleteChecklistItem(list, taskId, itemId);
|
||||
return ok({ ok: true, deletedItem: itemId });
|
||||
} catch (e) {
|
||||
return fail(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// ---- run ------------------------------------------------------------------
|
||||
|
||||
await server.connect(new StdioServerTransport());
|
||||
console.error(`[ms-todo] MCP server ready on stdio (${READONLY ? "read-only" : "read/write"}).`);
|
||||
Reference in New Issue
Block a user