@fswap/mcp-vikunja 0.1.1 → 0.1.3
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 +2 -2
- package/dist/index.js +54 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -88,7 +88,7 @@ VIKUNJA_API_TOKEN = "tk_..."
|
|
|
88
88
|
| `list_projects` | `GET /projects` | id, title, description, parent |
|
|
89
89
|
| `get_project` | `GET /projects/{id}` | |
|
|
90
90
|
| `create_project` | `PUT /projects` | optional parent project |
|
|
91
|
-
| `list_tasks` | `GET /tasks
|
|
91
|
+
| `list_tasks` | `GET /tasks` or `GET /projects/{id}/tasks` | open tasks by default; `assignedToMe`, `filter`, `sortBy`, pagination |
|
|
92
92
|
| `get_task` | `GET /tasks/{id}` | full task incl. description, labels, assignees |
|
|
93
93
|
| `create_task` | `PUT /projects/{id}/tasks` | title, description, dates, priority, labels |
|
|
94
94
|
| `update_task` | `POST /tasks/{id}` | merges your changes onto the current task; `labelIds` replaces labels |
|
|
@@ -97,7 +97,7 @@ VIKUNJA_API_TOKEN = "tk_..."
|
|
|
97
97
|
| `create_label` | `PUT /labels` | |
|
|
98
98
|
| `delete_task` | `DELETE /tasks/{id}` | only when delete is allowed (setup answer or `VIKUNJA_ALLOW_DELETE=true`) |
|
|
99
99
|
|
|
100
|
-
Vikunja's zero date (`0001-01-01T00:00:00Z`) is normalised to `null` in every response. API errors are returned to the model as `isError` results rather than crashing the server.
|
|
100
|
+
Every task and project includes a `url` to its page in the Vikunja web UI (taken from `/info` `frontend_url`). Vikunja's zero date (`0001-01-01T00:00:00Z`) is normalised to `null` in every response. API errors are returned to the model as `isError` results rather than crashing the server.
|
|
101
101
|
|
|
102
102
|
## Development
|
|
103
103
|
|
package/dist/index.js
CHANGED
|
@@ -180,10 +180,11 @@ function stripUndefined(obj) {
|
|
|
180
180
|
}
|
|
181
181
|
//#endregion
|
|
182
182
|
//#region src/tools/projects.ts
|
|
183
|
-
function summarizeProject(p) {
|
|
183
|
+
function summarizeProject(p, base) {
|
|
184
184
|
return {
|
|
185
185
|
id: p.id,
|
|
186
186
|
title: p.title,
|
|
187
|
+
url: `${base}/projects/${p.id}`,
|
|
187
188
|
description: p.description || null,
|
|
188
189
|
identifier: p.identifier || null,
|
|
189
190
|
parentProjectId: p.parent_project_id || null,
|
|
@@ -191,6 +192,16 @@ function summarizeProject(p) {
|
|
|
191
192
|
favorite: Boolean(p.is_favorite)
|
|
192
193
|
};
|
|
193
194
|
}
|
|
195
|
+
let frontendBase$1 = null;
|
|
196
|
+
async function frontendUrl$1(vikunja) {
|
|
197
|
+
if (frontendBase$1) return frontendBase$1;
|
|
198
|
+
try {
|
|
199
|
+
frontendBase$1 = ((await vikunja.get("/info")).frontend_url || vikunja.baseUrl).replace(/\/+$/, "");
|
|
200
|
+
} catch {
|
|
201
|
+
frontendBase$1 = vikunja.baseUrl;
|
|
202
|
+
}
|
|
203
|
+
return frontendBase$1;
|
|
204
|
+
}
|
|
194
205
|
function registerProjectTools(server, vikunja) {
|
|
195
206
|
server.registerTool("list_projects", {
|
|
196
207
|
title: "List projects",
|
|
@@ -202,19 +213,21 @@ function registerProjectTools(server, vikunja) {
|
|
|
202
213
|
perPage: z.number().int().min(1).max(100).default(50)
|
|
203
214
|
}
|
|
204
215
|
}, guard(async ({ search, includeArchived, page, perPage }) => {
|
|
205
|
-
|
|
216
|
+
const data = await vikunja.get("/projects", {
|
|
206
217
|
s: search,
|
|
207
218
|
is_archived: includeArchived ? true : void 0,
|
|
208
219
|
page,
|
|
209
220
|
per_page: perPage
|
|
210
|
-
})
|
|
221
|
+
});
|
|
222
|
+
const base = await frontendUrl$1(vikunja);
|
|
223
|
+
return ok(data.map((p) => summarizeProject(p, base)));
|
|
211
224
|
}));
|
|
212
225
|
server.registerTool("get_project", {
|
|
213
226
|
title: "Get project",
|
|
214
227
|
description: "Get one Vikunja project by id.",
|
|
215
228
|
inputSchema: { id: z.number().int().describe("Project id") }
|
|
216
229
|
}, guard(async ({ id }) => {
|
|
217
|
-
return ok(summarizeProject(await vikunja.get(`/projects/${id}`)));
|
|
230
|
+
return ok(summarizeProject(await vikunja.get(`/projects/${id}`), await frontendUrl$1(vikunja)));
|
|
218
231
|
}));
|
|
219
232
|
server.registerTool("create_project", {
|
|
220
233
|
title: "Create project",
|
|
@@ -228,27 +241,40 @@ function registerProjectTools(server, vikunja) {
|
|
|
228
241
|
const body = { title };
|
|
229
242
|
if (description !== void 0) body.description = description;
|
|
230
243
|
if (parentProjectId !== void 0) body.parent_project_id = parentProjectId;
|
|
231
|
-
return ok(summarizeProject(await vikunja.put("/projects", body)));
|
|
244
|
+
return ok(summarizeProject(await vikunja.put("/projects", body), await frontendUrl$1(vikunja)));
|
|
232
245
|
}));
|
|
233
246
|
}
|
|
234
247
|
//#endregion
|
|
235
248
|
//#region src/tools/tasks.ts
|
|
236
|
-
|
|
249
|
+
let frontendBase = null;
|
|
250
|
+
/** Resolve the web UI base URL once (Vikunja's API host may differ from its frontend host). */
|
|
251
|
+
async function frontendUrl(vikunja) {
|
|
252
|
+
if (frontendBase) return frontendBase;
|
|
253
|
+
try {
|
|
254
|
+
frontendBase = ((await vikunja.get("/info")).frontend_url || vikunja.baseUrl).replace(/\/+$/, "");
|
|
255
|
+
} catch {
|
|
256
|
+
frontendBase = vikunja.baseUrl;
|
|
257
|
+
}
|
|
258
|
+
return frontendBase;
|
|
259
|
+
}
|
|
260
|
+
function summarizeTask(t, base) {
|
|
237
261
|
return {
|
|
238
262
|
id: t.id,
|
|
239
263
|
title: t.title,
|
|
264
|
+
url: `${base}/tasks/${t.id}`,
|
|
240
265
|
done: t.done,
|
|
241
266
|
dueDate: normalizeDate(t.due_date),
|
|
242
267
|
priority: t.priority ?? 0,
|
|
243
268
|
projectId: t.project_id,
|
|
244
269
|
identifier: t.identifier || null,
|
|
245
270
|
labels: (t.labels ?? []).map((l) => l.title),
|
|
271
|
+
assignees: (t.assignees ?? []).map((u) => u.username),
|
|
246
272
|
updated: t.updated
|
|
247
273
|
};
|
|
248
274
|
}
|
|
249
|
-
function fullTask(t) {
|
|
275
|
+
function fullTask(t, base) {
|
|
250
276
|
return {
|
|
251
|
-
...summarizeTask(t),
|
|
277
|
+
...summarizeTask(t, base),
|
|
252
278
|
description: t.description || "",
|
|
253
279
|
doneAt: normalizeDate(t.done_at),
|
|
254
280
|
startDate: normalizeDate(t.start_date),
|
|
@@ -277,10 +303,11 @@ async function setLabels(vikunja, taskId, labelIds) {
|
|
|
277
303
|
function registerTaskTools(server, vikunja, { allowDelete = false, defaultProjectId = null } = {}) {
|
|
278
304
|
server.registerTool("list_tasks", {
|
|
279
305
|
title: "List tasks",
|
|
280
|
-
description: "List tasks, across all projects or within one project. Returns id, title, done, due date, priority
|
|
306
|
+
description: "List tasks, across all projects or within one project. Returns id, title, done, due date, priority, label names, assignee usernames and a web url (no descriptions — use get_task). By default only open tasks are returned. Set assignedToMe=true for the current user's tasks. For advanced queries pass a raw Vikunja `filter` string such as `done = false && due_date < now+7d`, `labels in 3, 5` or `assignees in alice`.",
|
|
281
307
|
inputSchema: {
|
|
282
308
|
projectId: z.number().int().optional().describe("Limit to this project (omit for all projects)"),
|
|
283
309
|
includeDone: z.boolean().default(false).describe("Include completed tasks"),
|
|
310
|
+
assignedToMe: z.boolean().default(false).describe("Only tasks assigned to the authenticated user"),
|
|
284
311
|
search: z.string().optional().describe("Full-text search in title/description"),
|
|
285
312
|
filter: z.string().optional().describe("Raw Vikunja filter expression; overrides includeDone. Fields: done, due_date, priority, labels, assignees, project"),
|
|
286
313
|
sortBy: z.enum([
|
|
@@ -296,24 +323,33 @@ function registerTaskTools(server, vikunja, { allowDelete = false, defaultProjec
|
|
|
296
323
|
page: z.number().int().min(1).default(1),
|
|
297
324
|
perPage: z.number().int().min(1).max(100).default(50)
|
|
298
325
|
}
|
|
299
|
-
}, guard(async ({ projectId, includeDone, search, filter, sortBy, orderBy, page, perPage }) => {
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
326
|
+
}, guard(async ({ projectId, includeDone, assignedToMe, search, filter, sortBy, orderBy, page, perPage }) => {
|
|
327
|
+
const clauses = [];
|
|
328
|
+
if (filter) clauses.push(`(${filter})`);
|
|
329
|
+
else if (!includeDone) clauses.push("done = false");
|
|
330
|
+
if (assignedToMe) {
|
|
331
|
+
const me = await vikunja.get("/user");
|
|
332
|
+
clauses.push(`assignees in '${me.username}'`);
|
|
333
|
+
}
|
|
334
|
+
const effectiveFilter = clauses.length > 0 ? clauses.join(" && ") : void 0;
|
|
335
|
+
const path = projectId != null ? `/projects/${projectId}/tasks` : "/tasks";
|
|
336
|
+
const data = await vikunja.get(path, {
|
|
303
337
|
s: search,
|
|
304
338
|
filter: effectiveFilter,
|
|
305
339
|
sort_by: sortBy,
|
|
306
340
|
order_by: orderBy,
|
|
307
341
|
page,
|
|
308
342
|
per_page: perPage
|
|
309
|
-
})
|
|
343
|
+
});
|
|
344
|
+
const base = await frontendUrl(vikunja);
|
|
345
|
+
return ok(data.map((t) => summarizeTask(t, base)));
|
|
310
346
|
}));
|
|
311
347
|
server.registerTool("get_task", {
|
|
312
348
|
title: "Get task",
|
|
313
349
|
description: "Get a single task with full details: description, dates, priority, labels and assignees.",
|
|
314
350
|
inputSchema: { id: z.number().int().describe("Task id") }
|
|
315
351
|
}, guard(async ({ id }) => {
|
|
316
|
-
return ok(fullTask(await vikunja.get(`/tasks/${id}`)));
|
|
352
|
+
return ok(fullTask(await vikunja.get(`/tasks/${id}`), await frontendUrl(vikunja)));
|
|
317
353
|
}));
|
|
318
354
|
server.registerTool("create_task", {
|
|
319
355
|
title: "Create task",
|
|
@@ -342,7 +378,7 @@ function registerTaskTools(server, vikunja, { allowDelete = false, defaultProjec
|
|
|
342
378
|
await setLabels(vikunja, task.id, labelIds);
|
|
343
379
|
task = await vikunja.get(`/tasks/${task.id}`);
|
|
344
380
|
}
|
|
345
|
-
return ok(fullTask(task));
|
|
381
|
+
return ok(fullTask(task, await frontendUrl(vikunja)));
|
|
346
382
|
}));
|
|
347
383
|
server.registerTool("update_task", {
|
|
348
384
|
title: "Update task",
|
|
@@ -383,7 +419,7 @@ function registerTaskTools(server, vikunja, { allowDelete = false, defaultProjec
|
|
|
383
419
|
await setLabels(vikunja, id, labelIds);
|
|
384
420
|
task = await vikunja.get(`/tasks/${id}`);
|
|
385
421
|
}
|
|
386
|
-
return ok(fullTask(task));
|
|
422
|
+
return ok(fullTask(task, await frontendUrl(vikunja)));
|
|
387
423
|
}));
|
|
388
424
|
server.registerTool("complete_task", {
|
|
389
425
|
title: "Complete task",
|
|
@@ -397,7 +433,7 @@ function registerTaskTools(server, vikunja, { allowDelete = false, defaultProjec
|
|
|
397
433
|
return ok(summarizeTask(await vikunja.post(`/tasks/${id}`, {
|
|
398
434
|
...current,
|
|
399
435
|
done
|
|
400
|
-
})));
|
|
436
|
+
}), await frontendUrl(vikunja)));
|
|
401
437
|
}));
|
|
402
438
|
server.registerTool("list_labels", {
|
|
403
439
|
title: "List labels",
|