feat(mcp): expand Vikunja server with project, delete, and comment tools
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m20s
Monorepo Pipeline / 🏗️ Build (push) Successful in 3m3s
Monorepo Pipeline / 🧹 Lint (push) Successful in 3m13s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m20s
Monorepo Pipeline / 🏗️ Build (push) Successful in 3m3s
Monorepo Pipeline / 🧹 Lint (push) Successful in 3m13s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
This commit is contained in:
180
packages/vikunja-mcp/src/index.test.ts
Normal file
180
packages/vikunja-mcp/src/index.test.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
// Mock axios
|
||||
const mockGet = vi.fn();
|
||||
const mockPost = vi.fn();
|
||||
const mockPut = vi.fn();
|
||||
const mockDelete = vi.fn();
|
||||
|
||||
vi.mock('axios', () => {
|
||||
return {
|
||||
default: {
|
||||
create: vi.fn().mockImplementation(() => ({
|
||||
get: mockGet,
|
||||
post: mockPost,
|
||||
put: mockPut,
|
||||
delete: mockDelete,
|
||||
})),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Set env vars
|
||||
process.env.VIKUNJA_API_TOKEN = 'test-token';
|
||||
process.env.VIKUNJA_BASE_URL = 'https://tasks.test.me';
|
||||
|
||||
// Mock express to avoid starting a real server
|
||||
vi.mock('express', () => {
|
||||
const mockApp = {
|
||||
use: vi.fn(),
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
listen: vi.fn().mockImplementation((port, host, cb) => cb && cb()),
|
||||
};
|
||||
const mockExpress = () => mockApp;
|
||||
(mockExpress as any).json = () => vi.fn();
|
||||
return { default: mockExpress };
|
||||
});
|
||||
|
||||
let server: any;
|
||||
|
||||
describe('Vikunja MCP Server - New Tools (RED)', () => {
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
if (!server) {
|
||||
const mod = await import('./index.js');
|
||||
server = mod.server;
|
||||
}
|
||||
});
|
||||
|
||||
it('should support vikunja_create_project', async () => {
|
||||
mockPut.mockResolvedValueOnce({
|
||||
data: { id: 10, title: 'New Project', description: 'Testing' }
|
||||
});
|
||||
|
||||
const handler = (server as any)._requestHandlers.get('tools/call');
|
||||
expect(handler).toBeDefined();
|
||||
|
||||
const response = await handler({
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'vikunja_create_project',
|
||||
arguments: {
|
||||
title: 'New Project',
|
||||
description: 'Testing',
|
||||
hex_color: 'ff0000',
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockPut).toHaveBeenCalledWith('/projects', {
|
||||
title: 'New Project',
|
||||
description: 'Testing',
|
||||
hex_color: 'ff0000',
|
||||
});
|
||||
expect(response.isError).toBeUndefined();
|
||||
expect(JSON.parse(response.content[0].text)).toEqual({
|
||||
id: 10,
|
||||
title: 'New Project',
|
||||
description: 'Testing',
|
||||
});
|
||||
});
|
||||
|
||||
it('should support vikunja_get_task', async () => {
|
||||
mockGet.mockResolvedValueOnce({
|
||||
data: { id: 42, title: 'My Task', project_id: 1 }
|
||||
});
|
||||
|
||||
const handler = (server as any)._requestHandlers.get('tools/call');
|
||||
const response = await handler({
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'vikunja_get_task',
|
||||
arguments: {
|
||||
task_id: 42,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockGet).toHaveBeenCalledWith('/tasks/42');
|
||||
expect(response.isError).toBeUndefined();
|
||||
expect(JSON.parse(response.content[0].text)).toEqual({
|
||||
id: 42,
|
||||
title: 'My Task',
|
||||
project_id: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('should support vikunja_delete_task', async () => {
|
||||
mockDelete.mockResolvedValueOnce({
|
||||
data: { message: 'success' }
|
||||
});
|
||||
|
||||
const handler = (server as any)._requestHandlers.get('tools/call');
|
||||
const response = await handler({
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'vikunja_delete_task',
|
||||
arguments: {
|
||||
task_id: 42,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockDelete).toHaveBeenCalledWith('/tasks/42');
|
||||
expect(response.isError).toBeUndefined();
|
||||
expect(JSON.parse(response.content[0].text)).toEqual({
|
||||
message: 'success',
|
||||
});
|
||||
});
|
||||
|
||||
it('should support vikunja_create_task_comment', async () => {
|
||||
mockPut.mockResolvedValueOnce({
|
||||
data: { id: 1, comment: 'New Comment', task_id: 42 }
|
||||
});
|
||||
|
||||
const handler = (server as any)._requestHandlers.get('tools/call');
|
||||
const response = await handler({
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'vikunja_create_task_comment',
|
||||
arguments: {
|
||||
task_id: 42,
|
||||
comment: 'New Comment',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockPut).toHaveBeenCalledWith('/tasks/42/comments', {
|
||||
comment: 'New Comment',
|
||||
});
|
||||
expect(response.isError).toBeUndefined();
|
||||
expect(JSON.parse(response.content[0].text)).toEqual({
|
||||
id: 1,
|
||||
comment: 'New Comment',
|
||||
task_id: 42,
|
||||
});
|
||||
});
|
||||
|
||||
it('should support vikunja_get_task_comments', async () => {
|
||||
mockGet.mockResolvedValueOnce({
|
||||
data: [{ id: 1, comment: 'New Comment', task_id: 42 }]
|
||||
});
|
||||
|
||||
const handler = (server as any)._requestHandlers.get('tools/call');
|
||||
const response = await handler({
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'vikunja_get_task_comments',
|
||||
arguments: {
|
||||
task_id: 42,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockGet).toHaveBeenCalledWith('/tasks/42/comments');
|
||||
expect(response.isError).toBeUndefined();
|
||||
expect(JSON.parse(response.content[0].text)).toEqual([
|
||||
{ id: 1, comment: 'New Comment', task_id: 42 }
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -83,8 +83,71 @@ const UPDATE_TASK_TOOL: Tool = {
|
||||
},
|
||||
};
|
||||
|
||||
const CREATE_PROJECT_TOOL: Tool = {
|
||||
name: "vikunja_create_project",
|
||||
description: "Create a new project (list) in Vikunja",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
title: { type: "string", description: "The title of the project" },
|
||||
description: { type: "string", description: "Optional description of the project" },
|
||||
hex_color: { type: "string", description: "Optional hex color without the '#' prefix" }
|
||||
},
|
||||
required: ["title"]
|
||||
}
|
||||
};
|
||||
|
||||
const GET_TASK_TOOL: Tool = {
|
||||
name: "vikunja_get_task",
|
||||
description: "Get details for a specific task by ID",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
task_id: { type: "number", description: "The ID of the task" }
|
||||
},
|
||||
required: ["task_id"]
|
||||
}
|
||||
};
|
||||
|
||||
const DELETE_TASK_TOOL: Tool = {
|
||||
name: "vikunja_delete_task",
|
||||
description: "Delete a task by ID",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
task_id: { type: "number", description: "The ID of the task to delete" }
|
||||
},
|
||||
required: ["task_id"]
|
||||
}
|
||||
};
|
||||
|
||||
const CREATE_TASK_COMMENT_TOOL: Tool = {
|
||||
name: "vikunja_create_task_comment",
|
||||
description: "Add a comment to a task",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
task_id: { type: "number", description: "The ID of the task to comment on" },
|
||||
comment: { type: "string", description: "The comment text" }
|
||||
},
|
||||
required: ["task_id", "comment"]
|
||||
}
|
||||
};
|
||||
|
||||
const GET_TASK_COMMENTS_TOOL: Tool = {
|
||||
name: "vikunja_get_task_comments",
|
||||
description: "Get all comments for a specific task",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
task_id: { type: "number", description: "The ID of the task" }
|
||||
},
|
||||
required: ["task_id"]
|
||||
}
|
||||
};
|
||||
|
||||
// --- Server Setup ---
|
||||
const server = new Server(
|
||||
export const server = new Server(
|
||||
{ name: "vikunja-mcp", version: "1.0.0" },
|
||||
{ capabilities: { tools: {} } }
|
||||
);
|
||||
@@ -94,7 +157,12 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
GET_PROJECTS_TOOL,
|
||||
GET_TASKS_TOOL,
|
||||
CREATE_TASK_TOOL,
|
||||
UPDATE_TASK_TOOL
|
||||
UPDATE_TASK_TOOL,
|
||||
CREATE_PROJECT_TOOL,
|
||||
GET_TASK_TOOL,
|
||||
DELETE_TASK_TOOL,
|
||||
CREATE_TASK_COMMENT_TOOL,
|
||||
GET_TASK_COMMENTS_TOOL
|
||||
],
|
||||
}));
|
||||
|
||||
@@ -133,6 +201,42 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
||||
}
|
||||
|
||||
if (request.params.name === "vikunja_create_project") {
|
||||
const { title, description, hex_color } = request.params.arguments as any;
|
||||
// Vikunja standard: creating a new root resource uses PUT /projects in this version
|
||||
const res = await api.put('/projects', {
|
||||
title,
|
||||
description: description || "",
|
||||
hex_color: hex_color || ""
|
||||
});
|
||||
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
||||
}
|
||||
|
||||
if (request.params.name === "vikunja_get_task") {
|
||||
const { task_id } = request.params.arguments as any;
|
||||
const res = await api.get(`/tasks/${task_id}`);
|
||||
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
||||
}
|
||||
|
||||
if (request.params.name === "vikunja_delete_task") {
|
||||
const { task_id } = request.params.arguments as any;
|
||||
const res = await api.delete(`/tasks/${task_id}`);
|
||||
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
||||
}
|
||||
|
||||
if (request.params.name === "vikunja_create_task_comment") {
|
||||
const { task_id, comment } = request.params.arguments as any;
|
||||
// Vikunja standard: task sub-resources (like comments) are typically appended using PUT /tasks/{id}/comments
|
||||
const res = await api.put(`/tasks/${task_id}/comments`, { comment });
|
||||
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
||||
}
|
||||
|
||||
if (request.params.name === "vikunja_get_task_comments") {
|
||||
const { task_id } = request.params.arguments as any;
|
||||
const res = await api.get(`/tasks/${task_id}/comments`);
|
||||
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
||||
}
|
||||
|
||||
throw new Error(`Unknown tool: ${request.params.name}`);
|
||||
} catch (e: any) {
|
||||
const msg = e.response?.data?.message || e.message;
|
||||
|
||||
Reference in New Issue
Block a user