trollo-mcp-server 1.0.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 ADDED
@@ -0,0 +1,145 @@
1
+ # Trollo MCP Server
2
+
3
+ An [MCP (Model Context Protocol)](https://modelcontextprotocol.io) server that lets AI agents manage [Trollo](https://trollo.lol) kanban boards. Works with any MCP-compatible client: Cursor, Claude Desktop, and others.
4
+
5
+ ## Quick Start
6
+
7
+ ### Using npx (no install)
8
+
9
+ The fastest way to get started. Your MCP client runs the server on demand:
10
+
11
+ ```json
12
+ {
13
+ "mcpServers": {
14
+ "trollo": {
15
+ "command": "npx",
16
+ "args": ["-y", "trollo-mcp-server"],
17
+ "env": {
18
+ "TROLLO_URL": "https://trollo.lol",
19
+ "TROLLO_API_KEY": "trlo_your_key_here"
20
+ }
21
+ }
22
+ }
23
+ }
24
+ ```
25
+
26
+ ### Global install
27
+
28
+ ```bash
29
+ npm install -g trollo-mcp-server
30
+ ```
31
+
32
+ Then configure your client to run `trollo-mcp`:
33
+
34
+ ```json
35
+ {
36
+ "mcpServers": {
37
+ "trollo": {
38
+ "command": "trollo-mcp",
39
+ "env": {
40
+ "TROLLO_URL": "https://trollo.lol",
41
+ "TROLLO_API_KEY": "trlo_your_key_here"
42
+ }
43
+ }
44
+ }
45
+ }
46
+ ```
47
+
48
+ ### From source (this repo)
49
+
50
+ ```bash
51
+ cd mcp
52
+ npm install
53
+ npm run build
54
+ ```
55
+
56
+ Then point your client to the local build:
57
+
58
+ ```json
59
+ {
60
+ "mcpServers": {
61
+ "trollo": {
62
+ "command": "node",
63
+ "args": ["mcp/dist/index.js"],
64
+ "env": {
65
+ "TROLLO_URL": "https://trollo.lol",
66
+ "TROLLO_API_KEY": "trlo_your_key_here"
67
+ }
68
+ }
69
+ }
70
+ }
71
+ ```
72
+
73
+ ## Configuration
74
+
75
+ | Environment Variable | Description |
76
+ |---|---|
77
+ | `TROLLO_URL` | Base URL of the Trollo instance (e.g. `https://trollo.lol`) |
78
+ | `TROLLO_API_KEY` | API key starting with `trlo_` — create one in Trollo under Settings > API Keys |
79
+
80
+ ## Where to put the config
81
+
82
+ | Client | Config file |
83
+ |---|---|
84
+ | Cursor | `.cursor/mcp.json` in your project or `~/.cursor/mcp.json` globally |
85
+ | Claude Desktop | `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows) |
86
+
87
+ ## Tools
88
+
89
+ ### Reading
90
+
91
+ | Tool | Description |
92
+ |---|---|
93
+ | `list_boards` | List all boards accessible to the API key |
94
+ | `get_board` | Get full board state: lists, cards, checklists, comments, labels, users |
95
+
96
+ ### Lists
97
+
98
+ | Tool | Parameters | Description |
99
+ |---|---|---|
100
+ | `add_list` | `boardId`, `title`, `position?` | Add a new list |
101
+ | `rename_list` | `boardId`, `listId`, `title` | Rename a list |
102
+ | `move_list` | `boardId`, `listId`, `position` | Reorder a list |
103
+ | `remove_list` | `boardId`, `listId` | Remove a list |
104
+
105
+ ### Cards
106
+
107
+ | Tool | Parameters | Description |
108
+ |---|---|---|
109
+ | `add_card` | `boardId`, `listId`, `title` | Add a card to a list |
110
+ | `move_card` | `boardId`, `cardId`, `toListId`, `position` | Move a card |
111
+ | `remove_card` | `boardId`, `cardId`, `fromListId` | Remove a card |
112
+ | `edit_card` | `boardId`, `cardId`, `title?`, `description?` | Edit title and/or description |
113
+ | `set_card_due` | `boardId`, `cardId`, `due?` | Set or remove due date |
114
+ | `set_card_points` | `boardId`, `cardId`, `points?` | Set or remove story points |
115
+ | `manage_card_label` | `boardId`, `cardId`, `labelIndex`, `action` | Add or remove a label |
116
+
117
+ ### Comments
118
+
119
+ | Tool | Parameters | Description |
120
+ |---|---|---|
121
+ | `add_comment` | `boardId`, `cardId`, `body` | Add a comment (supports markdown) |
122
+
123
+ ### Checklists
124
+
125
+ | Tool | Parameters | Description |
126
+ |---|---|---|
127
+ | `add_checklist` | `boardId`, `cardId`, `title` | Add a checklist to a card |
128
+ | `add_checklist_item` | `boardId`, `cardId`, `checklistId`, `title` | Add a checklist item |
129
+ | `toggle_checklist_item` | `boardId`, `cardId`, `itemId`, `checked` | Check or uncheck an item |
130
+
131
+ ## API Key Permissions
132
+
133
+ Each API key in Trollo has per-action permissions. If a tool call is denied, the key may not have the required permission enabled. Manage permissions in Trollo under Settings > API Keys.
134
+
135
+ ## Publishing
136
+
137
+ To publish a new version to npm:
138
+
139
+ ```bash
140
+ cd mcp
141
+ npm version patch # or minor / major
142
+ npm publish
143
+ ```
144
+
145
+ The `prepublishOnly` script builds automatically before publishing.
@@ -0,0 +1,27 @@
1
+ export interface TrolloBoard {
2
+ id: string;
3
+ title: string;
4
+ shared: number;
5
+ archived?: boolean;
6
+ groupId?: number;
7
+ isOwner: boolean;
8
+ role: number | string | null;
9
+ }
10
+ export interface ActionResult {
11
+ result: string;
12
+ userId?: number;
13
+ value?: string | number | null;
14
+ }
15
+ export declare class TrolloClient {
16
+ private baseUrl;
17
+ private apiKey;
18
+ constructor(baseUrl: string, apiKey: string);
19
+ private request;
20
+ listBoards(): Promise<{
21
+ boards: TrolloBoard[];
22
+ }>;
23
+ getBoard(boardId: string): Promise<{
24
+ board: Record<string, unknown>;
25
+ }>;
26
+ performAction(boardId: string, type: string, data: unknown[]): Promise<ActionResult>;
27
+ }
package/dist/client.js ADDED
@@ -0,0 +1,33 @@
1
+ export class TrolloClient {
2
+ baseUrl;
3
+ apiKey;
4
+ constructor(baseUrl, apiKey) {
5
+ this.baseUrl = baseUrl.replace(/\/+$/, '');
6
+ this.apiKey = apiKey;
7
+ }
8
+ async request(method, path, body) {
9
+ const url = `${this.baseUrl}${path}`;
10
+ const res = await fetch(url, {
11
+ method,
12
+ headers: {
13
+ 'Authorization': `Bearer ${this.apiKey}`,
14
+ ...(body !== undefined ? { 'Content-Type': 'application/json' } : {}),
15
+ },
16
+ body: body !== undefined ? JSON.stringify(body) : undefined,
17
+ });
18
+ if (!res.ok) {
19
+ const text = await res.text().catch(() => res.statusText);
20
+ throw new Error(`Trollo API ${method} ${path} failed (${res.status}): ${text}`);
21
+ }
22
+ return res.json();
23
+ }
24
+ async listBoards() {
25
+ return this.request('GET', '/api/v1/boards');
26
+ }
27
+ async getBoard(boardId) {
28
+ return this.request('GET', `/api/v1/boards/${encodeURIComponent(boardId)}`);
29
+ }
30
+ async performAction(boardId, type, data) {
31
+ return this.request('POST', `/api/v1/boards/${encodeURIComponent(boardId)}/action`, { type, data });
32
+ }
33
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { TrolloClient } from './client.js';
5
+ import { registerBoardTools } from './tools/boards.js';
6
+ import { registerListTools } from './tools/lists.js';
7
+ import { registerCardTools } from './tools/cards.js';
8
+ import { registerCommentTools } from './tools/comments.js';
9
+ import { registerChecklistTools } from './tools/checklists.js';
10
+ const url = process.env.TROLLO_URL;
11
+ const key = process.env.TROLLO_API_KEY;
12
+ if (!url || !key) {
13
+ console.error('Missing required environment variables: TROLLO_URL and TROLLO_API_KEY');
14
+ process.exit(1);
15
+ }
16
+ const client = new TrolloClient(url, key);
17
+ const server = new McpServer({ name: 'trollo', version: '1.0.0' }, {
18
+ capabilities: { tools: {} },
19
+ instructions: [
20
+ 'Trollo is a Trello-like kanban board app.',
21
+ '',
22
+ 'Data model: Board contains labels[] ({color, title?} indexed 0-based), users[] ({id, name, email}), and lists[] ({id, title}). Each list contains cards[] ({id, title, slug, description, due?, points?}). Each card has labels[] (array of label indices), users[] (array of user IDs), checklists[] ({id, title} with items[] ({id, title, checked?})), comments[] ({id, body, userId}), and files[] ({id, name, url}).',
23
+ '',
24
+ 'All entity IDs are integers. Always call get_board first to discover IDs before performing actions.',
25
+ '',
26
+ 'Workflow patterns:',
27
+ '- Reading: call list_boards to find board IDs, then get_board for the full state.',
28
+ '- Adding cards: get_board to find the target listId, then add_card with boardId, listId, and title.',
29
+ '- Moving cards between lists: get_board to read source list cards, then move_card for each card with its id, the destination listId, and position.',
30
+ '- Checklists: add_checklist on a card, note the returned checklist ID, then add_checklist_item for each item.',
31
+ '',
32
+ 'Limitations: encrypted boards are not accessible, file uploads are not supported, viewer-role keys cannot write, each API key has per-action permissions.',
33
+ ].join('\n'),
34
+ });
35
+ registerBoardTools(server, client);
36
+ registerListTools(server, client);
37
+ registerCardTools(server, client);
38
+ registerCommentTools(server, client);
39
+ registerChecklistTools(server, client);
40
+ const transport = new StdioServerTransport();
41
+ await server.connect(transport);
@@ -0,0 +1,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { TrolloClient } from '../client.js';
3
+ export declare function registerBoardTools(server: McpServer, client: TrolloClient): void;
@@ -0,0 +1,15 @@
1
+ import { z } from 'zod';
2
+ export function registerBoardTools(server, client) {
3
+ server.tool('list_boards', 'List all Trollo boards accessible to the configured API key', async () => {
4
+ const { boards } = await client.listBoards();
5
+ return {
6
+ content: [{ type: 'text', text: JSON.stringify(boards, null, 2) }],
7
+ };
8
+ });
9
+ server.tool('get_board', 'Get full board state including lists, cards, labels, users, checklists, and comments', { boardId: z.string().describe('The board ID') }, async ({ boardId }) => {
10
+ const { board } = await client.getBoard(boardId);
11
+ return {
12
+ content: [{ type: 'text', text: JSON.stringify(board, null, 2) }],
13
+ };
14
+ });
15
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { TrolloClient } from '../client.js';
3
+ export declare function registerCardTools(server: McpServer, client: TrolloClient): void;
@@ -0,0 +1,76 @@
1
+ import { z } from 'zod';
2
+ export function registerCardTools(server, client) {
3
+ server.tool('add_card', 'Add a new card to a list', {
4
+ boardId: z.string().describe('The board ID'),
5
+ listId: z.number().int().describe('The list ID to add the card to'),
6
+ title: z.string().describe('Title for the new card'),
7
+ }, async ({ boardId, listId, title }) => {
8
+ const result = await client.performAction(boardId, 'CardAdded', [listId, title]);
9
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
10
+ });
11
+ server.tool('move_card', 'Move a card to a different list and/or position', {
12
+ boardId: z.string().describe('The board ID'),
13
+ cardId: z.number().int().describe('The card ID to move'),
14
+ toListId: z.number().int().describe('Destination list ID'),
15
+ position: z.number().int().describe('Position index in the destination list'),
16
+ }, async ({ boardId, cardId, toListId, position }) => {
17
+ const result = await client.performAction(boardId, 'CardMoved', [cardId, toListId, position]);
18
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
19
+ });
20
+ server.tool('remove_card', 'Remove a card from a list', {
21
+ boardId: z.string().describe('The board ID'),
22
+ cardId: z.number().int().describe('The card ID to remove'),
23
+ fromListId: z.number().int().describe('The list ID the card belongs to'),
24
+ }, async ({ boardId, cardId, fromListId }) => {
25
+ const result = await client.performAction(boardId, 'CardRemoved', [cardId, fromListId]);
26
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
27
+ });
28
+ server.tool('edit_card', 'Edit a card\'s title and/or description. Provide at least one of title or description.', {
29
+ boardId: z.string().describe('The board ID'),
30
+ cardId: z.number().int().describe('The card ID to edit'),
31
+ title: z.string().optional().describe('New title (omit to leave unchanged)'),
32
+ description: z.string().optional().describe('New description in markdown (omit to leave unchanged)'),
33
+ }, async ({ boardId, cardId, title, description }) => {
34
+ const results = [];
35
+ if (title !== undefined) {
36
+ results.push(await client.performAction(boardId, 'CardTitleChange', [cardId, title]));
37
+ }
38
+ if (description !== undefined) {
39
+ results.push(await client.performAction(boardId, 'CardDescriptionChange', [cardId, description]));
40
+ }
41
+ if (results.length === 0) {
42
+ return { content: [{ type: 'text', text: 'No changes specified — provide title and/or description.' }], isError: true };
43
+ }
44
+ return { content: [{ type: 'text', text: JSON.stringify(results.length === 1 ? results[0] : results) }] };
45
+ });
46
+ server.tool('set_card_due', 'Set or remove a card\'s due date', {
47
+ boardId: z.string().describe('The board ID'),
48
+ cardId: z.number().int().describe('The card ID'),
49
+ due: z.number().optional().describe('Due date as Unix timestamp in milliseconds. Omit to remove the due date.'),
50
+ }, async ({ boardId, cardId, due }) => {
51
+ const result = due !== undefined
52
+ ? await client.performAction(boardId, 'CardDueSet', [cardId, due])
53
+ : await client.performAction(boardId, 'CardDueRemoved', [cardId]);
54
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
55
+ });
56
+ server.tool('set_card_points', 'Set or remove story points on a card', {
57
+ boardId: z.string().describe('The board ID'),
58
+ cardId: z.number().int().describe('The card ID'),
59
+ points: z.number().optional().describe('Story points value. Omit to remove.'),
60
+ }, async ({ boardId, cardId, points }) => {
61
+ const result = points !== undefined
62
+ ? await client.performAction(boardId, 'CardPointsSet', [cardId, points])
63
+ : await client.performAction(boardId, 'CardPointsRemoved', [cardId]);
64
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
65
+ });
66
+ server.tool('manage_card_label', 'Add or remove a label on a card', {
67
+ boardId: z.string().describe('The board ID'),
68
+ cardId: z.number().int().describe('The card ID'),
69
+ labelIndex: z.number().int().describe('Index of the label in the board\'s labels array'),
70
+ action: z.enum(['add', 'remove']).describe('Whether to add or remove the label'),
71
+ }, async ({ boardId, cardId, labelIndex, action }) => {
72
+ const type = action === 'add' ? 'CardLabelAdd' : 'CardLabelRemove';
73
+ const result = await client.performAction(boardId, type, [cardId, labelIndex]);
74
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
75
+ });
76
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { TrolloClient } from '../client.js';
3
+ export declare function registerChecklistTools(server: McpServer, client: TrolloClient): void;
@@ -0,0 +1,30 @@
1
+ import { z } from 'zod';
2
+ export function registerChecklistTools(server, client) {
3
+ server.tool('add_checklist', 'Add a checklist to a card', {
4
+ boardId: z.string().describe('The board ID'),
5
+ cardId: z.number().int().describe('The card ID to add the checklist to'),
6
+ title: z.string().describe('Checklist title'),
7
+ }, async ({ boardId, cardId, title }) => {
8
+ const result = await client.performAction(boardId, 'CLAdded', [cardId, title]);
9
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
10
+ });
11
+ server.tool('add_checklist_item', 'Add an item to an existing checklist', {
12
+ boardId: z.string().describe('The board ID'),
13
+ cardId: z.number().int().describe('The card ID'),
14
+ checklistId: z.number().int().describe('The checklist ID'),
15
+ title: z.string().describe('Item title'),
16
+ }, async ({ boardId, cardId, checklistId, title }) => {
17
+ const result = await client.performAction(boardId, 'CLIAdded', [cardId, checklistId, title]);
18
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
19
+ });
20
+ server.tool('toggle_checklist_item', 'Check or uncheck a checklist item', {
21
+ boardId: z.string().describe('The board ID'),
22
+ cardId: z.number().int().describe('The card ID'),
23
+ itemId: z.number().int().describe('The checklist item ID'),
24
+ checked: z.boolean().describe('true to check, false to uncheck'),
25
+ }, async ({ boardId, cardId, itemId, checked }) => {
26
+ const type = checked ? 'CLIChecked' : 'CLIUnchecked';
27
+ const result = await client.performAction(boardId, type, [cardId, itemId]);
28
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
29
+ });
30
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { TrolloClient } from '../client.js';
3
+ export declare function registerCommentTools(server: McpServer, client: TrolloClient): void;
@@ -0,0 +1,11 @@
1
+ import { z } from 'zod';
2
+ export function registerCommentTools(server, client) {
3
+ server.tool('add_comment', 'Add a comment to a card', {
4
+ boardId: z.string().describe('The board ID'),
5
+ cardId: z.number().int().describe('The card ID to comment on'),
6
+ body: z.string().describe('Comment body (supports markdown)'),
7
+ }, async ({ boardId, cardId, body }) => {
8
+ const result = await client.performAction(boardId, 'CMAdded', [cardId, body]);
9
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
10
+ });
11
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { TrolloClient } from '../client.js';
3
+ export declare function registerListTools(server: McpServer, client: TrolloClient): void;
@@ -0,0 +1,34 @@
1
+ import { z } from 'zod';
2
+ export function registerListTools(server, client) {
3
+ server.tool('add_list', 'Add a new list to a board', {
4
+ boardId: z.string().describe('The board ID'),
5
+ title: z.string().describe('Title for the new list'),
6
+ position: z.number().int().optional().describe('Position index to insert at (appends to end if omitted)'),
7
+ }, async ({ boardId, title, position }) => {
8
+ const result = await client.performAction(boardId, 'ListAdded', [position ?? null, title]);
9
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
10
+ });
11
+ server.tool('rename_list', 'Rename an existing list', {
12
+ boardId: z.string().describe('The board ID'),
13
+ listId: z.number().int().describe('The list ID'),
14
+ title: z.string().describe('New title for the list'),
15
+ }, async ({ boardId, listId, title }) => {
16
+ const result = await client.performAction(boardId, 'ListRenamed', [listId, title]);
17
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
18
+ });
19
+ server.tool('move_list', 'Move a list to a new position on the board', {
20
+ boardId: z.string().describe('The board ID'),
21
+ listId: z.number().int().describe('The list ID'),
22
+ position: z.number().int().describe('New position index'),
23
+ }, async ({ boardId, listId, position }) => {
24
+ const result = await client.performAction(boardId, 'ListMoved', [listId, position]);
25
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
26
+ });
27
+ server.tool('remove_list', 'Remove a list from the board', {
28
+ boardId: z.string().describe('The board ID'),
29
+ listId: z.number().int().describe('The list ID to remove'),
30
+ }, async ({ boardId, listId }) => {
31
+ const result = await client.performAction(boardId, 'ListRemoved', [listId]);
32
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
33
+ });
34
+ }
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "trollo-mcp-server",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for managing Trollo kanban boards from AI agents",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "trollo-mcp": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "prepublishOnly": "npm run build",
16
+ "start": "node dist/index.js"
17
+ },
18
+ "keywords": [
19
+ "mcp",
20
+ "model-context-protocol",
21
+ "trollo",
22
+ "kanban",
23
+ "ai",
24
+ "agent"
25
+ ],
26
+ "license": "none",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/Q42/Trollo",
30
+ "directory": "mcp"
31
+ },
32
+ "dependencies": {
33
+ "@modelcontextprotocol/sdk": "^1.12.1"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^25.5.0",
37
+ "typescript": "^5.8.2"
38
+ }
39
+ }