planko-mcp-server 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,10 +4,12 @@ MCP server for syncing [Planko](https://planko.io) tasks with local Markdown fil
4
4
 
5
5
  ## Features
6
6
 
7
- - **3 tools**: setup, sync preview, sync
7
+ - **10 tools**: 3 folder-sync tools + 7 standalone task/note CRUD tools
8
+ - **Type-aware sync**: each folder syncs either **tasks** (type=1) or **notes** (type=2), chosen at setup
8
9
  - **Multi-folder**: sync multiple projects to different local folders
9
10
  - **Bidirectional sync**: pull remote changes, push local changes
10
11
  - **Delete sync**: deleted local files remove tasks on server; deleted tasks remove local files
12
+ - **Standalone CRUD**: create/edit/complete/delete tasks and notes directly, no folder setup required
11
13
  - **User-scoped API key**: one key works across all your projects
12
14
 
13
15
  ## Getting Started
@@ -64,10 +66,15 @@ Restart Claude Code, Cursor, or whichever MCP client you use so it picks up the
64
66
  Ask your AI agent to run:
65
67
 
66
68
  ```
67
- planko_setup(projectName: "My Project", folderPath: "/path/to/folder", email: "you@email.com")
69
+ planko_setup(projectName: "My Project", folderPath: "/path/to/folder", email: "you@email.com", syncType: "tasks")
68
70
  ```
69
71
 
70
- This maps a Planko project to a local folder. You can set up multiple projects pointing to different folders.
72
+ This maps a Planko project to a local folder. `syncType` is **required** and must be `"tasks"` or `"notes"`:
73
+
74
+ - `syncType: "tasks"` — the folder pulls/pushes only **tasks** (type=1). New local `.md` files are created as tasks.
75
+ - `syncType: "notes"` — the folder pulls/pushes only **notes** (type=2). New local `.md` files are created as notes.
76
+
77
+ A folder is bound to one type for its whole lifetime. To sync both tasks and notes for the same project, set up two folders. You can set up multiple projects pointing to different folders.
71
78
 
72
79
  ### Step 5 — Sync
73
80
 
@@ -79,12 +86,66 @@ Tasks are pulled as `.md` files into your folder. Local changes are pushed back
79
86
 
80
87
  ## Tools
81
88
 
89
+ The server exposes 10 tools in two groups: **folder-sync** tools (bind a project to a local folder) and **standalone CRUD** tools (operate directly via your API key, no folder needed).
90
+
91
+ ### Folder-sync tools
92
+
82
93
  | Tool | Description | Parameters |
83
94
  |---|---|---|
84
- | `planko_setup` | Set up sync between a project and a local folder | `projectName`, `folderPath`, `email` |
95
+ | `planko_setup` | Set up sync between a project and a local folder | `projectName`, `folderPath`, `email`, `syncType` (`tasks`\|`notes`) |
85
96
  | `planko_sync_preview` | Preview what would be synced (read-only) | `projectName` (optional) |
86
97
  | `planko_sync` | Execute bidirectional sync with delete support | `projectName` (optional) |
87
98
 
99
+ ### Standalone CRUD tools
100
+
101
+ These work with the **same API key** and require **no folder setup**. Notes are Planko tasks with `type=2`; tasks are `type=1`. They live in the same collection, so edit/delete work on both. `create_*` tools accept all task/note properties as optional params (only `name` is required); the Markdown `description` is converted to Planko's rich-text (BlockNote) format automatically.
102
+
103
+ | Tool | Description | Key parameters |
104
+ |---|---|---|
105
+ | `planko_create_task` | Create a task (type=1) | `name` (required), `projectName` (optional), plus any properties below |
106
+ | `planko_create_note` | Create a note (type=2) | `name` (required), `projectName` (optional), plus any properties below |
107
+ | `planko_edit_task` | Edit an existing task by id | `taskId` (required), plus any properties to change |
108
+ | `planko_edit_note` | Edit an existing note by id (shares the task edit endpoint) | `taskId` (required), plus any properties to change |
109
+ | `planko_complete_task` | Mark a task complete (status=2) | `taskId` (required) |
110
+ | `planko_delete_task` | Delete a task by id | `taskId` (required) |
111
+ | `planko_delete_note` | Delete a note by id (shares the task delete endpoint) | `taskId` (required) |
112
+
113
+ **Optional properties** accepted by the create/edit tools (all optional; omit to leave unchanged):
114
+
115
+ | Property | Type | Notes |
116
+ |---|---|---|
117
+ | `description` | Markdown string | Converted to BlockNote JSON before saving |
118
+ | `dueDate` | ISO 8601 string | |
119
+ | `datePlain` | `YYYY-MM-DD` | |
120
+ | `time` / `endTime` | `HH:MM` | Start / end time |
121
+ | `alert` | boolean | Whether a reminder is enabled |
122
+ | `alertLeadTime` | `null`\|`ontime`\|`15min`\|`30min`\|`1hour`\|`1day` | Reminder lead time |
123
+ | `priority` | `1`\|`2`\|`3` | |
124
+ | `repeat` | `null`\|`daily`\|`workdays`\|`weekly`\|`biweekly`\|`monthly`\|`custom_weekly`\|`yearly` | Recurrence rule |
125
+ | `repeatDate` | ISO 8601 string | Recurrence anchor/end |
126
+ | `selectedWeekdays` | array of `0`–`6` | For `custom_weekly` (0=Sunday) |
127
+ | `parentId` | ObjectId | Parent task, for subtasks |
128
+ | `tags` | array of ObjectId | Tag ids |
129
+ | `kanbanColumnId` / `boardId` | ObjectId | Kanban placement |
130
+ | `type` | `1`\|`2` | (edit tools only) switch task↔note |
131
+ | `projectId` | ObjectId | (edit tools only) move to another project |
132
+
133
+ Notes:
134
+
135
+ - On create, `projectName` is resolved to a project id via your accessible projects (not the local folder config). If omitted, the backend applies your default project.
136
+ - `complete` sets status=2 and, for a recurring task, stops the series and completes the current occurrence.
137
+ - Reminders, recurrence, positions, kanban and timezone handling are all applied server-side (the tools reuse Planko's own task logic).
138
+
139
+ #### Examples
140
+
141
+ ```
142
+ planko_create_task(name: "Ship v2", projectName: "Work", dueDate: "2026-08-01", priority: 1)
143
+ planko_create_note(name: "Meeting notes", description: "# Sync\n- point A\n- point B")
144
+ planko_edit_task(taskId: "665f...c0", name: "Ship v2.1", alertLeadTime: "1hour")
145
+ planko_complete_task(taskId: "665f...c0")
146
+ planko_delete_note(taskId: "665f...aa")
147
+ ```
148
+
88
149
  ### Sync preview
89
150
 
90
151
  ```
@@ -112,10 +173,14 @@ Sync order: delete, pull, push. Local changes overwrite remote on conflict.
112
173
 
113
174
  ## How it works
114
175
 
115
- - Each Planko task maps to a local `.md` file (task name becomes filename)
176
+ - Each Planko task/note maps to a local `.md` file (task name becomes filename)
177
+ - Each folder is **type-bound**: `planko_setup` records `type` (1=tasks, 2=notes) on the folder's sync state and in the global config, and sync only pulls/pushes that type. New local `.md` files are created with the folder's type.
178
+ - The chosen `type` is sent to the backend on every `status`/`pull` (`&type=`) and `push` (body `type`) call.
179
+ - Backward-compatible: a config entry created before this feature has no `type`, so its folder keeps the legacy type-agnostic behavior (syncs all task types).
116
180
  - Task descriptions are converted between BlockNote and Markdown automatically
117
- - Sync state is tracked in `.planko-mcp-sync.json` per folder
118
- - Global config is stored at `~/.planko-mcp/config.json`
181
+ - Sync state is tracked in `.planko-mcp-sync.json` per folder (includes `syncType` when set)
182
+ - Global config is stored at `~/.planko-mcp/config.json` (each project entry stores `projectId`, `folder`, `email`, `type`)
183
+ - The standalone CRUD tools bypass folders entirely and call user-scoped endpoints directly with your API key
119
184
  - API keys are never written to any file
120
185
 
121
186
  ## License
package/index.js CHANGED
@@ -24,6 +24,7 @@ import {
24
24
  blockNoteToMarkdown,
25
25
  markdownToBlockNote,
26
26
  descriptionToMarkdown,
27
+ markdownToDescription,
27
28
  } from './src/converters.js';
28
29
  import {
29
30
  readConfig,
@@ -71,9 +72,10 @@ function toolError(text) {
71
72
  async function computeSyncDiff(projectId, folder, sync) {
72
73
  const localFiles = listLocalFiles(folder);
73
74
  const { byFileName, byId } = buildIndexes(sync.tasks);
75
+ const syncType = sync.syncType;
74
76
 
75
77
  // Get ALL tasks from server (no mcpLastSyncDate filter) to detect deletions
76
- const statusData = await api.status(projectId);
78
+ const statusData = await api.status(projectId, null, syncType);
77
79
  const remoteTasks = statusData.tasks || [];
78
80
  const remoteIdSet = new Set(remoteTasks.map((t) => t._id.toString()));
79
81
 
@@ -169,10 +171,11 @@ async function computeSyncDiff(projectId, folder, sync) {
169
171
  async function executeSync(projectId, folder, sync) {
170
172
  const localFiles = listLocalFiles(folder);
171
173
  const prePullSyncDate = sync.mcpLastSyncDate;
174
+ const syncType = sync.syncType;
172
175
  const results = [];
173
176
 
174
177
  // Get ALL tasks to detect deletions
175
- const allStatusData = await api.status(projectId);
178
+ const allStatusData = await api.status(projectId, null, syncType);
176
179
  const allRemoteTasks = allStatusData.tasks || [];
177
180
  const remoteIdSet = new Set(allRemoteTasks.map((t) => t._id.toString()));
178
181
 
@@ -214,7 +217,7 @@ async function executeSync(projectId, folder, sync) {
214
217
  }
215
218
 
216
219
  if (toDeleteRemote.length > 0) {
217
- const deleteResponse = await api.push(projectId, toDeleteRemote);
220
+ const deleteResponse = await api.push(projectId, toDeleteRemote, syncType);
218
221
  const deletedRemote = (deleteResponse.tasks || []).filter((t) => t.deleted);
219
222
  // Remove deleted tasks from sync state
220
223
  const deletedIds = new Set(deletedRemote.map((t) => t._id.toString()));
@@ -223,7 +226,7 @@ async function executeSync(projectId, folder, sync) {
223
226
  }
224
227
 
225
228
  // --- PULL phase ---
226
- const pullData = await api.pull(projectId, prePullSyncDate);
229
+ const pullData = await api.pull(projectId, prePullSyncDate, syncType);
227
230
  const pulledTasks = pullData.tasks || [];
228
231
 
229
232
  const { byId } = buildIndexes(sync.tasks);
@@ -308,7 +311,7 @@ async function executeSync(projectId, folder, sync) {
308
311
  });
309
312
  }
310
313
 
311
- const pushResponse = await api.push(projectId, pushItems);
314
+ const pushResponse = await api.push(projectId, pushItems, syncType);
312
315
  const responseTasks = (pushResponse.tasks || []).filter((t) => !t.deleted);
313
316
 
314
317
  const { byId: pushIdIdx, byFileName: pushFnIdx } = buildIndexes(sync.tasks);
@@ -363,13 +366,16 @@ const server = new McpServer({
363
366
  // ---- planko_setup ----
364
367
  server.tool(
365
368
  'planko_setup',
366
- 'Set up sync between a Planko project and a local folder. Supports multiple project-folder mappings. The user must provide the project name, local folder path, and email.',
369
+ 'Set up sync between a Planko project and a local folder. Supports multiple project-folder mappings. The user must provide the project name, local folder path, email, and whether the folder syncs tasks or notes.',
367
370
  {
368
371
  projectName: z.string().describe('Name of the Planko project to sync'),
369
372
  folderPath: z.string().describe('Absolute path to the local folder for .md task files'),
370
373
  email: z.string().email().describe('User email for task attribution'),
374
+ syncType: z
375
+ .enum(['tasks', 'notes'])
376
+ .describe("Whether this folder syncs 'tasks' (type=1) or 'notes' (type=2)"),
371
377
  },
372
- async ({ projectName, folderPath, email }) => {
378
+ async ({ projectName, folderPath, email, syncType }) => {
373
379
  try {
374
380
  // List user's projects to find the matching one
375
381
  const { projects } = await api.projects();
@@ -390,6 +396,8 @@ server.tool(
390
396
  mkdirSync(folderPath, { recursive: true });
391
397
  }
392
398
 
399
+ const type = syncType === 'notes' ? 2 : 1;
400
+
393
401
  // Save to global config
394
402
  const config = readConfig();
395
403
  config.projects = config.projects || {};
@@ -398,20 +406,24 @@ server.tool(
398
406
  folder: folderPath,
399
407
  email,
400
408
  isWorkspace: match.isWorkspace,
409
+ type,
401
410
  };
402
411
  writeConfig(config);
403
412
 
404
413
  // Initialize sync state for this folder
405
- const syncState = createSyncState(match._id, match.name);
414
+ const syncState = createSyncState(match._id, match.name, type);
406
415
  writeSyncState(folderPath, syncState);
407
416
 
417
+ const typeLabel = type === 2 ? 'Notes' : 'Tasks';
418
+
408
419
  return toolOk(
409
420
  `Setup complete for project "${match.name}".\n` +
410
421
  ` Project ID: ${match._id}\n` +
411
422
  ` Folder: ${folderPath}\n` +
412
423
  ` Email: ${email}\n` +
424
+ ` Syncs: ${typeLabel} (type=${type})\n` +
413
425
  ` Workspace: ${match.isWorkspace ? 'Yes' : 'No (personal)'}\n\n` +
414
- `Run planko_sync to pull tasks into this folder.`
426
+ `Run planko_sync to pull ${typeLabel.toLowerCase()} into this folder.`
415
427
  );
416
428
  } catch (err) {
417
429
  return toolError(`Setup failed: ${err.message}`);
@@ -540,7 +552,8 @@ server.tool(
540
552
  for (const target of targets) {
541
553
  let sync = readSyncState(target.folder);
542
554
  if (!sync) {
543
- sync = createSyncState(target.projectId, target.name);
555
+ // target.type is undefined for pre-feature config entries → legacy type-agnostic sync.
556
+ sync = createSyncState(target.projectId, target.name, target.type);
544
557
  }
545
558
 
546
559
  allResults.push(`--- ${target.name} (${target.folder}) ---`);
@@ -561,6 +574,281 @@ server.tool(
561
574
  }
562
575
  );
563
576
 
577
+ // --- Standalone CRUD tools (Part B) ---
578
+ // These operate directly via the user-scoped API key and do NOT require any
579
+ // folder to be configured with planko_setup. Notes are Task docs with type=2;
580
+ // tasks are type=1. Edit/Delete work on both; Complete is task-oriented.
581
+
582
+ const objectId = z
583
+ .string()
584
+ .regex(/^[a-fA-F\d]{24}$/, 'Must be a 24-character hex ObjectId');
585
+
586
+ const timeRegex = /^([01]?[0-9]|2[0-3]):[0-5][0-9]$/;
587
+
588
+ // Shared editable task/note properties (all optional). `description` is Markdown
589
+ // here and is converted to BlockNote JSON before being sent to the backend.
590
+ const taskProps = {
591
+ description: z
592
+ .string()
593
+ .optional()
594
+ .describe('Body as Markdown (converted to BlockNote JSON before saving)'),
595
+ dueDate: z.string().optional().describe('Due date (ISO 8601 string)'),
596
+ datePlain: z
597
+ .string()
598
+ .regex(/^\d{4}-\d{2}-\d{2}$/, 'Must be YYYY-MM-DD')
599
+ .optional()
600
+ .describe('Plain date, YYYY-MM-DD'),
601
+ time: z.string().regex(timeRegex, 'Must be HH:MM').optional().describe('Start time, HH:MM'),
602
+ endTime: z.string().regex(timeRegex, 'Must be HH:MM').optional().describe('End time, HH:MM'),
603
+ alert: z.boolean().optional().describe('Whether an alert/reminder is enabled'),
604
+ alertLeadTime: z
605
+ .enum(['ontime', '15min', '30min', '1hour', '1day'])
606
+ .nullable()
607
+ .optional()
608
+ .describe('Reminder lead time (null or one of ontime/15min/30min/1hour/1day)'),
609
+ priority: z
610
+ .union([z.literal(1), z.literal(2), z.literal(3)])
611
+ .optional()
612
+ .describe('Priority: 1, 2, or 3'),
613
+ repeat: z
614
+ .enum(['daily', 'workdays', 'weekly', 'biweekly', 'monthly', 'custom_weekly', 'yearly'])
615
+ .nullable()
616
+ .optional()
617
+ .describe('Recurrence rule (null or one of the recurrence keywords)'),
618
+ repeatDate: z.string().optional().describe('Recurrence end/anchor date (ISO 8601 string)'),
619
+ selectedWeekdays: z
620
+ .array(z.number().int().min(0).max(6))
621
+ .optional()
622
+ .describe('Weekdays for custom_weekly repeat (0=Sunday..6=Saturday)'),
623
+ parentId: objectId.optional().describe('Parent task id (for subtasks)'),
624
+ tags: z.array(objectId).optional().describe('Array of tag ids'),
625
+ kanbanColumnId: objectId.optional().describe('Kanban column id'),
626
+ boardId: objectId.optional().describe('Board id'),
627
+ };
628
+
629
+ /**
630
+ * Build a request body from tool params: drop undefined values and convert the
631
+ * Markdown `description` into BlockNote JSON.
632
+ */
633
+ function buildTaskBody(params) {
634
+ const body = {};
635
+ for (const [key, value] of Object.entries(params)) {
636
+ if (value === undefined) continue;
637
+ body[key] = value;
638
+ }
639
+ if ('description' in body) {
640
+ body.description = markdownToDescription(body.description);
641
+ }
642
+ return body;
643
+ }
644
+
645
+ /**
646
+ * Resolve a project name to a project id via the user's accessible projects
647
+ * (NOT the local folder config), so create tools work without any setup.
648
+ */
649
+ async function resolveProjectId(projectName) {
650
+ const { projects } = await api.projects();
651
+ const match = projects.find(
652
+ (p) => p.name.toLowerCase() === projectName.toLowerCase()
653
+ );
654
+ if (!match) {
655
+ const available = projects.map((p) => ` - ${p.name}`).join('\n');
656
+ throw new Error(
657
+ `Project "${projectName}" not found.\n\nAvailable projects:\n${available}`
658
+ );
659
+ }
660
+ return match._id;
661
+ }
662
+
663
+ /** Extract { id, name } from a create/edit response (shape-tolerant). */
664
+ function describeTask(res) {
665
+ const t = (res && (res.task || res.data)) || res || {};
666
+ return {
667
+ id: t._id || t.id || 'unknown',
668
+ name: t.name != null ? t.name : 'unknown',
669
+ };
670
+ }
671
+
672
+ /**
673
+ * Shared create handler for tasks (type=1) and notes (type=2).
674
+ */
675
+ async function handleCreate(type, params, kindLabel) {
676
+ const { name, projectName, ...rest } = params;
677
+ const body = buildTaskBody(rest);
678
+ body.name = name;
679
+ body.type = type;
680
+
681
+ // Resolve projectName -> projectId via the API. When omitted, OMIT the key
682
+ // entirely so the backend applies the user's default project.
683
+ if (projectName) {
684
+ body.projectId = await resolveProjectId(projectName);
685
+ }
686
+
687
+ const res = await api.createTask(body);
688
+ const { id, name: savedName } = describeTask(res);
689
+ const where = body.projectId
690
+ ? ` in project ${body.projectId}`
691
+ : ' in your default project';
692
+ return toolOk(`Created ${kindLabel} "${savedName}" (id: ${id})${where}.`);
693
+ }
694
+
695
+ /**
696
+ * Shared edit handler for tasks and notes (same endpoint).
697
+ */
698
+ async function handleEdit(params, kindLabel) {
699
+ const { taskId, ...rest } = params;
700
+ const body = buildTaskBody(rest);
701
+ const changed = Object.keys(body);
702
+ if (changed.length === 0) {
703
+ return toolError('Nothing to update: provide at least one field to change.');
704
+ }
705
+ const res = await api.updateTask(taskId, body);
706
+ const { id, name: savedName } = describeTask(res);
707
+ return toolOk(
708
+ `Updated ${kindLabel} "${savedName}" (id: ${id}). Changed: ${changed.join(', ')}.`
709
+ );
710
+ }
711
+
712
+ // ---- planko_create_task ----
713
+ server.tool(
714
+ 'planko_create_task',
715
+ 'Create a Planko task (type=1) directly via your API key. Works without any folder setup. Provide a name (required); all other properties are optional. Optionally target a project by name (otherwise your default project is used).',
716
+ {
717
+ name: z.string().describe('Task name (required)'),
718
+ projectName: z
719
+ .string()
720
+ .optional()
721
+ .describe('Project name to create the task in (optional — omit for your default project)'),
722
+ ...taskProps,
723
+ },
724
+ async (params) => {
725
+ try {
726
+ return await handleCreate(1, params, 'task');
727
+ } catch (err) {
728
+ return toolError(`Create task failed: ${err.message}`);
729
+ }
730
+ }
731
+ );
732
+
733
+ // ---- planko_create_note ----
734
+ server.tool(
735
+ 'planko_create_note',
736
+ 'Create a Planko note (type=2) directly via your API key. Works without any folder setup. Provide a name (required); all other properties are optional. Optionally target a project by name (otherwise your default project is used).',
737
+ {
738
+ name: z.string().describe('Note name (required)'),
739
+ projectName: z
740
+ .string()
741
+ .optional()
742
+ .describe('Project name to create the note in (optional — omit for your default project)'),
743
+ ...taskProps,
744
+ },
745
+ async (params) => {
746
+ try {
747
+ return await handleCreate(2, params, 'note');
748
+ } catch (err) {
749
+ return toolError(`Create note failed: ${err.message}`);
750
+ }
751
+ }
752
+ );
753
+
754
+ // ---- planko_edit_task ----
755
+ server.tool(
756
+ 'planko_edit_task',
757
+ 'Edit an existing Planko task by id. Provide the taskId and any properties to change. The Markdown description is converted to BlockNote JSON.',
758
+ {
759
+ taskId: objectId.describe('Id of the task to edit (required)'),
760
+ name: z.string().optional().describe('New task name'),
761
+ type: z
762
+ .union([z.literal(1), z.literal(2)])
763
+ .optional()
764
+ .describe('Change type: 1=task, 2=note'),
765
+ projectId: objectId.optional().describe('Move to a different project by id'),
766
+ ...taskProps,
767
+ },
768
+ async (params) => {
769
+ try {
770
+ return await handleEdit(params, 'task');
771
+ } catch (err) {
772
+ return toolError(`Edit task failed: ${err.message}`);
773
+ }
774
+ }
775
+ );
776
+
777
+ // ---- planko_edit_note ----
778
+ server.tool(
779
+ 'planko_edit_note',
780
+ 'Edit an existing Planko note by id (shares the task edit endpoint). Provide the taskId and any properties to change. The Markdown description is converted to BlockNote JSON.',
781
+ {
782
+ taskId: objectId.describe('Id of the note to edit (required)'),
783
+ name: z.string().optional().describe('New note name'),
784
+ type: z
785
+ .union([z.literal(1), z.literal(2)])
786
+ .optional()
787
+ .describe('Change type: 1=task, 2=note'),
788
+ projectId: objectId.optional().describe('Move to a different project by id'),
789
+ ...taskProps,
790
+ },
791
+ async (params) => {
792
+ try {
793
+ return await handleEdit(params, 'note');
794
+ } catch (err) {
795
+ return toolError(`Edit note failed: ${err.message}`);
796
+ }
797
+ }
798
+ );
799
+
800
+ // ---- planko_complete_task ----
801
+ server.tool(
802
+ 'planko_complete_task',
803
+ 'Mark a Planko task complete (status=2) by id.',
804
+ {
805
+ taskId: objectId.describe('Id of the task to complete (required)'),
806
+ },
807
+ async ({ taskId }) => {
808
+ try {
809
+ const res = await api.completeTask(taskId);
810
+ const { id, name } = describeTask(res);
811
+ return toolOk(`Completed task "${name}" (id: ${id}).`);
812
+ } catch (err) {
813
+ return toolError(`Complete task failed: ${err.message}`);
814
+ }
815
+ }
816
+ );
817
+
818
+ // ---- planko_delete_task ----
819
+ server.tool(
820
+ 'planko_delete_task',
821
+ 'Delete a Planko task by id.',
822
+ {
823
+ taskId: objectId.describe('Id of the task to delete (required)'),
824
+ },
825
+ async ({ taskId }) => {
826
+ try {
827
+ await api.deleteTask(taskId);
828
+ return toolOk(`Deleted task (id: ${taskId}).`);
829
+ } catch (err) {
830
+ return toolError(`Delete task failed: ${err.message}`);
831
+ }
832
+ }
833
+ );
834
+
835
+ // ---- planko_delete_note ----
836
+ server.tool(
837
+ 'planko_delete_note',
838
+ 'Delete a Planko note by id (shares the task delete endpoint).',
839
+ {
840
+ taskId: objectId.describe('Id of the note to delete (required)'),
841
+ },
842
+ async ({ taskId }) => {
843
+ try {
844
+ await api.deleteTask(taskId);
845
+ return toolOk(`Deleted note (id: ${taskId}).`);
846
+ } catch (err) {
847
+ return toolError(`Delete note failed: ${err.message}`);
848
+ }
849
+ }
850
+ );
851
+
564
852
  // --- Start server ---
565
853
 
566
854
  async function main() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "planko-mcp-server",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "MCP server for syncing Planko tasks with local Markdown files",
5
5
  "type": "module",
6
6
  "bin": {
package/src/api.js CHANGED
@@ -51,35 +51,74 @@ export function createApiClient({ apiKey, apiBase }) {
51
51
  },
52
52
 
53
53
  /**
54
- * GET /mcp-project-sync/status?projectId=...&mcpLastSyncDate=...
54
+ * GET /mcp-project-sync/status?projectId=...&mcpLastSyncDate=...&type=...
55
+ * `type` (1=tasks, 2=notes) is appended whenever set, independent of mcpLastSyncDate.
55
56
  */
56
- async status(projectId, mcpLastSyncDate) {
57
+ async status(projectId, mcpLastSyncDate, type) {
57
58
  let qs = `projectId=${projectId}`;
58
59
  if (mcpLastSyncDate != null) {
59
60
  qs += `&mcpLastSyncDate=${mcpLastSyncDate}`;
60
61
  }
62
+ if (type === 1 || type === 2) {
63
+ qs += `&type=${type}`;
64
+ }
61
65
  return request('GET', `/mcp-project-sync/status?${qs}`);
62
66
  },
63
67
 
64
68
  /**
65
- * GET /mcp-project-sync/pull?projectId=...&mcpLastSyncDate=...
69
+ * GET /mcp-project-sync/pull?projectId=...&mcpLastSyncDate=...&type=...
66
70
  */
67
- async pull(projectId, mcpLastSyncDate) {
71
+ async pull(projectId, mcpLastSyncDate, type) {
68
72
  let qs = `projectId=${projectId}`;
69
73
  if (mcpLastSyncDate != null) {
70
74
  qs += `&mcpLastSyncDate=${mcpLastSyncDate}`;
71
75
  }
76
+ if (type === 1 || type === 2) {
77
+ qs += `&type=${type}`;
78
+ }
72
79
  return request('GET', `/mcp-project-sync/pull?${qs}`);
73
80
  },
74
81
 
75
82
  /**
76
83
  * POST /mcp-project-sync/push
84
+ * `type` (1=tasks, 2=notes) is included in the body whenever set.
85
+ */
86
+ async push(projectId, tasks, type) {
87
+ const body = { projectId, tasks };
88
+ if (type === 1 || type === 2) {
89
+ body.type = type;
90
+ }
91
+ return request('POST', '/mcp-project-sync/push', body);
92
+ },
93
+
94
+ // --- Standalone CRUD (user-scoped, no folder sync required) ---
95
+
96
+ /**
97
+ * POST /mcp-project-sync/tasks — create a task (type=1) or note (type=2).
98
+ */
99
+ async createTask(body) {
100
+ return request('POST', '/mcp-project-sync/tasks', body);
101
+ },
102
+
103
+ /**
104
+ * PATCH /mcp-project-sync/tasks/:taskId — edit a task or note.
105
+ */
106
+ async updateTask(taskId, body) {
107
+ return request('PATCH', `/mcp-project-sync/tasks/${taskId}`, body);
108
+ },
109
+
110
+ /**
111
+ * POST /mcp-project-sync/tasks/:taskId/complete — mark complete (status=2).
112
+ */
113
+ async completeTask(taskId) {
114
+ return request('POST', `/mcp-project-sync/tasks/${taskId}/complete`);
115
+ },
116
+
117
+ /**
118
+ * DELETE /mcp-project-sync/tasks/:taskId — delete a task or note.
77
119
  */
78
- async push(projectId, tasks) {
79
- return request('POST', '/mcp-project-sync/push', {
80
- projectId,
81
- tasks,
82
- });
120
+ async deleteTask(taskId) {
121
+ return request('DELETE', `/mcp-project-sync/tasks/${taskId}`);
83
122
  },
84
123
  };
85
124
  }
package/src/sync-state.js CHANGED
@@ -77,8 +77,8 @@ export function writeSyncState(folder, data) {
77
77
  writeFileSync(join(folder, SYNC_FILE), JSON.stringify(safe, null, 2));
78
78
  }
79
79
 
80
- export function createSyncState(projectId, projectName) {
81
- return {
80
+ export function createSyncState(projectId, projectName, syncType) {
81
+ const state = {
82
82
  project: {
83
83
  _id: projectId,
84
84
  name: projectName,
@@ -88,6 +88,11 @@ export function createSyncState(projectId, projectName) {
88
88
  mcpLastSyncPushChanges: [],
89
89
  tasks: [],
90
90
  };
91
+ // syncType: 1 = tasks, 2 = notes. Omitted (undefined) => legacy type-agnostic sync.
92
+ if (syncType === 1 || syncType === 2) {
93
+ state.syncType = syncType;
94
+ }
95
+ return state;
91
96
  }
92
97
 
93
98
  // --- Local file operations ---