@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.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/dist/index.js +54 -18
  3. 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/all` or `GET /projects/{id}/tasks` | open tasks by default; `filter`, `sortBy`, pagination |
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
- return ok((await vikunja.get("/projects", {
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
- })).map(summarizeProject));
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
- function summarizeTask(t) {
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 and label names (no descriptions — use get_task). By default only open tasks are returned. For advanced queries pass a raw Vikunja `filter` string such as `done = false && due_date < now+7d` or `labels in 3, 5`.",
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 effectiveFilter = filter ?? (includeDone ? void 0 : "done = false");
301
- const path = projectId != null ? `/projects/${projectId}/tasks` : "/tasks/all";
302
- return ok((await vikunja.get(path, {
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
- })).map(summarizeTask));
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fswap/mcp-vikunja",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "MCP server for Vikunja — list, create, update and complete tasks from Claude",
5
5
  "type": "module",
6
6
  "bin": {