@fswap/mcp-vikunja 0.1.2 → 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 +1 -1
  2. package/dist/index.js +42 -15
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -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,15 +241,27 @@ 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,
@@ -247,9 +272,9 @@ function summarizeTask(t) {
247
272
  updated: t.updated
248
273
  };
249
274
  }
250
- function fullTask(t) {
275
+ function fullTask(t, base) {
251
276
  return {
252
- ...summarizeTask(t),
277
+ ...summarizeTask(t, base),
253
278
  description: t.description || "",
254
279
  doneAt: normalizeDate(t.done_at),
255
280
  startDate: normalizeDate(t.start_date),
@@ -278,7 +303,7 @@ async function setLabels(vikunja, taskId, labelIds) {
278
303
  function registerTaskTools(server, vikunja, { allowDelete = false, defaultProjectId = null } = {}) {
279
304
  server.registerTool("list_tasks", {
280
305
  title: "List tasks",
281
- description: "List tasks, across all projects or within one project. Returns id, title, done, due date, priority, label names and assignee usernames (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`.",
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`.",
282
307
  inputSchema: {
283
308
  projectId: z.number().int().optional().describe("Limit to this project (omit for all projects)"),
284
309
  includeDone: z.boolean().default(false).describe("Include completed tasks"),
@@ -308,21 +333,23 @@ function registerTaskTools(server, vikunja, { allowDelete = false, defaultProjec
308
333
  }
309
334
  const effectiveFilter = clauses.length > 0 ? clauses.join(" && ") : void 0;
310
335
  const path = projectId != null ? `/projects/${projectId}/tasks` : "/tasks";
311
- return ok((await vikunja.get(path, {
336
+ const data = await vikunja.get(path, {
312
337
  s: search,
313
338
  filter: effectiveFilter,
314
339
  sort_by: sortBy,
315
340
  order_by: orderBy,
316
341
  page,
317
342
  per_page: perPage
318
- })).map(summarizeTask));
343
+ });
344
+ const base = await frontendUrl(vikunja);
345
+ return ok(data.map((t) => summarizeTask(t, base)));
319
346
  }));
320
347
  server.registerTool("get_task", {
321
348
  title: "Get task",
322
349
  description: "Get a single task with full details: description, dates, priority, labels and assignees.",
323
350
  inputSchema: { id: z.number().int().describe("Task id") }
324
351
  }, guard(async ({ id }) => {
325
- return ok(fullTask(await vikunja.get(`/tasks/${id}`)));
352
+ return ok(fullTask(await vikunja.get(`/tasks/${id}`), await frontendUrl(vikunja)));
326
353
  }));
327
354
  server.registerTool("create_task", {
328
355
  title: "Create task",
@@ -351,7 +378,7 @@ function registerTaskTools(server, vikunja, { allowDelete = false, defaultProjec
351
378
  await setLabels(vikunja, task.id, labelIds);
352
379
  task = await vikunja.get(`/tasks/${task.id}`);
353
380
  }
354
- return ok(fullTask(task));
381
+ return ok(fullTask(task, await frontendUrl(vikunja)));
355
382
  }));
356
383
  server.registerTool("update_task", {
357
384
  title: "Update task",
@@ -392,7 +419,7 @@ function registerTaskTools(server, vikunja, { allowDelete = false, defaultProjec
392
419
  await setLabels(vikunja, id, labelIds);
393
420
  task = await vikunja.get(`/tasks/${id}`);
394
421
  }
395
- return ok(fullTask(task));
422
+ return ok(fullTask(task, await frontendUrl(vikunja)));
396
423
  }));
397
424
  server.registerTool("complete_task", {
398
425
  title: "Complete task",
@@ -406,7 +433,7 @@ function registerTaskTools(server, vikunja, { allowDelete = false, defaultProjec
406
433
  return ok(summarizeTask(await vikunja.post(`/tasks/${id}`, {
407
434
  ...current,
408
435
  done
409
- })));
436
+ }), await frontendUrl(vikunja)));
410
437
  }));
411
438
  server.registerTool("list_labels", {
412
439
  title: "List labels",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fswap/mcp-vikunja",
3
- "version": "0.1.2",
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": {