kanbango 2.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/AGENTS.md +606 -0
- package/API.md +177 -0
- package/CHANGELOG.md +122 -0
- package/LICENSE +21 -0
- package/LLM_AGENTS.md +484 -0
- package/README.md +360 -0
- package/bin/kanban-cmd.js +40 -0
- package/bin/kanban.js +462 -0
- package/index.html +1095 -0
- package/index.js +15 -0
- package/kanban.js +625 -0
- package/kanbango.md +48 -0
- package/mcp-server.js +489 -0
- package/package.json +41 -0
- package/planv2.md +307 -0
- package/tests/run.js +19 -0
package/mcp-server.js
ADDED
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
|
|
4
|
+
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
|
|
5
|
+
const { CallToolRequestSchema, ListToolsRequestSchema } = require('@modelcontextprotocol/sdk/types.js');
|
|
6
|
+
const { spawn } = require('child_process');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const kanban = require('./kanban.js');
|
|
9
|
+
|
|
10
|
+
const COLS = kanban.COLS;
|
|
11
|
+
const READ_VIEWS = Object.keys(kanban.VIEW_FIELDS);
|
|
12
|
+
let guiProcess = null;
|
|
13
|
+
let guiPort = null;
|
|
14
|
+
|
|
15
|
+
function normalizePort(value) {
|
|
16
|
+
const parsed = Number.parseInt(value, 10);
|
|
17
|
+
if (!Number.isFinite(parsed) || parsed < 1 || parsed > 65535) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return parsed;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function serializeError(error) {
|
|
24
|
+
return {
|
|
25
|
+
error: {
|
|
26
|
+
code: error.code || 'INTERNAL_ERROR',
|
|
27
|
+
message: error.message,
|
|
28
|
+
hint: error.hint || 'Inspect the request payload and try again',
|
|
29
|
+
details: error.details || {},
|
|
30
|
+
retryable: Boolean(error.retryable)
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function invalidRequest(message, hint, details) {
|
|
36
|
+
return kanban.createKanbanError('VALIDATION_ERROR', message, hint, details, false, 400);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeReturnShape(returnShape) {
|
|
40
|
+
if (returnShape === undefined) return 'summary';
|
|
41
|
+
if (!['none', 'summary', 'full'].includes(returnShape)) {
|
|
42
|
+
throw invalidRequest(
|
|
43
|
+
`Unsupported return value: ${returnShape}`,
|
|
44
|
+
'Use one of: none, summary, full',
|
|
45
|
+
{ return: returnShape }
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
return returnShape;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function normalizeReadOptions(args, defaultView) {
|
|
52
|
+
if (args.fields !== undefined) {
|
|
53
|
+
if (!Array.isArray(args.fields) || args.fields.length === 0) {
|
|
54
|
+
throw invalidRequest(
|
|
55
|
+
'fields must be a non-empty array when provided',
|
|
56
|
+
'Pass fields like ["title", "description"] or omit the field',
|
|
57
|
+
{ fields: args.fields }
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { fields: args.fields };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const view = args.view || defaultView;
|
|
65
|
+
if (!READ_VIEWS.includes(view)) {
|
|
66
|
+
throw invalidRequest(
|
|
67
|
+
`Unsupported view: ${view}`,
|
|
68
|
+
`Use one of: ${READ_VIEWS.join(', ')}`,
|
|
69
|
+
{ view }
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { view };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function formatTaskResult(task, returnShape) {
|
|
77
|
+
if (returnShape === 'none') return { ok: true };
|
|
78
|
+
return kanban.shapeTask(task, { view: returnShape === 'full' ? 'full' : 'summary' });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function startGuiServer(port) {
|
|
82
|
+
const desiredPort = normalizePort(port ?? 5500);
|
|
83
|
+
if (!desiredPort) {
|
|
84
|
+
throw invalidRequest('Invalid port', 'Use an integer between 1 and 65535', { port });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (guiProcess && guiProcess.exitCode === null) {
|
|
88
|
+
return {
|
|
89
|
+
status: 'already_running',
|
|
90
|
+
port: guiPort,
|
|
91
|
+
url: `http://localhost:${guiPort}`
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
await kanban.ensureBacklogDir();
|
|
96
|
+
|
|
97
|
+
const scriptPath = path.join(__dirname, 'bin', 'kanban.js');
|
|
98
|
+
guiProcess = spawn(process.execPath, [scriptPath, 'serve', String(desiredPort)], {
|
|
99
|
+
stdio: 'ignore',
|
|
100
|
+
windowsHide: true
|
|
101
|
+
});
|
|
102
|
+
guiPort = desiredPort;
|
|
103
|
+
|
|
104
|
+
guiProcess.on('exit', () => {
|
|
105
|
+
guiProcess = null;
|
|
106
|
+
guiPort = null;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
status: 'started',
|
|
111
|
+
port: desiredPort,
|
|
112
|
+
pid: guiProcess.pid,
|
|
113
|
+
url: `http://localhost:${desiredPort}`
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function stopGuiServer() {
|
|
118
|
+
if (!guiProcess || guiProcess.exitCode !== null) {
|
|
119
|
+
guiProcess = null;
|
|
120
|
+
guiPort = null;
|
|
121
|
+
return { status: 'not_running' };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
guiProcess.kill();
|
|
125
|
+
return { status: 'stopping', port: guiPort };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function guiStatus() {
|
|
129
|
+
if (!guiProcess || guiProcess.exitCode !== null) {
|
|
130
|
+
return { status: 'not_running' };
|
|
131
|
+
}
|
|
132
|
+
return { status: 'running', port: guiPort, pid: guiProcess.pid, url: `http://localhost:${guiPort}` };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const server = new Server(
|
|
136
|
+
{
|
|
137
|
+
name: 'kanbango',
|
|
138
|
+
version: '2.0.0'
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
capabilities: {
|
|
142
|
+
tools: {}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
148
|
+
return {
|
|
149
|
+
tools: [
|
|
150
|
+
{
|
|
151
|
+
name: 'kanban_read',
|
|
152
|
+
description: 'Read tasks from kanban board with compact views or explicit fields.',
|
|
153
|
+
inputSchema: {
|
|
154
|
+
type: 'object',
|
|
155
|
+
properties: {
|
|
156
|
+
operation: {
|
|
157
|
+
type: 'string',
|
|
158
|
+
enum: ['list', 'show'],
|
|
159
|
+
description: "Operation to perform: 'list' for all tasks, 'show' for a specific task",
|
|
160
|
+
default: 'list'
|
|
161
|
+
},
|
|
162
|
+
task_id: {
|
|
163
|
+
type: 'string',
|
|
164
|
+
description: "Task ID (required for 'show' operation, e.g. 'PI-014-google-calendar')"
|
|
165
|
+
},
|
|
166
|
+
col: {
|
|
167
|
+
type: 'string',
|
|
168
|
+
enum: COLS,
|
|
169
|
+
description: 'Optional column filter for list'
|
|
170
|
+
},
|
|
171
|
+
epic: {
|
|
172
|
+
type: 'string',
|
|
173
|
+
description: 'Optional epic group filter for list'
|
|
174
|
+
},
|
|
175
|
+
view: {
|
|
176
|
+
type: 'string',
|
|
177
|
+
enum: READ_VIEWS,
|
|
178
|
+
description: 'Preset response view. Defaults to summary.'
|
|
179
|
+
},
|
|
180
|
+
fields: {
|
|
181
|
+
type: 'array',
|
|
182
|
+
description: 'Explicit fields to return. When provided, fields override view.',
|
|
183
|
+
items: { type: 'string' }
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
name: 'kanban_create',
|
|
190
|
+
description: 'Create a new task on the kanban board with optional rich planning fields.',
|
|
191
|
+
inputSchema: {
|
|
192
|
+
type: 'object',
|
|
193
|
+
properties: {
|
|
194
|
+
title: {
|
|
195
|
+
type: 'string',
|
|
196
|
+
description: 'Title of new task'
|
|
197
|
+
},
|
|
198
|
+
col: {
|
|
199
|
+
type: 'string',
|
|
200
|
+
enum: COLS,
|
|
201
|
+
default: 'planned',
|
|
202
|
+
description: 'Column to place task in (active|planned|icebox|done)'
|
|
203
|
+
},
|
|
204
|
+
epic: {
|
|
205
|
+
type: 'string',
|
|
206
|
+
default: '—',
|
|
207
|
+
description: 'Epic group name (optional)'
|
|
208
|
+
},
|
|
209
|
+
description: {
|
|
210
|
+
type: 'string',
|
|
211
|
+
description: 'High-level context and implementation plan'
|
|
212
|
+
},
|
|
213
|
+
specs: {
|
|
214
|
+
type: 'string',
|
|
215
|
+
description: 'Technical constraints, APIs, and edge cases'
|
|
216
|
+
},
|
|
217
|
+
acceptance_criteria: {
|
|
218
|
+
type: 'array',
|
|
219
|
+
description: 'What must be true for the task to be complete',
|
|
220
|
+
items: { type: 'string' }
|
|
221
|
+
},
|
|
222
|
+
subtasks: {
|
|
223
|
+
type: 'array',
|
|
224
|
+
description: 'Optional subtask list',
|
|
225
|
+
items: {
|
|
226
|
+
type: 'object',
|
|
227
|
+
properties: {
|
|
228
|
+
id: { type: 'string' },
|
|
229
|
+
text: { type: 'string' },
|
|
230
|
+
done: { type: 'boolean' },
|
|
231
|
+
description: { type: 'string' }
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
notes: {
|
|
236
|
+
type: 'string',
|
|
237
|
+
description: 'Optional freeform notes'
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
required: ['title']
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
name: 'kanban_update',
|
|
245
|
+
description: 'Move tasks, toggle subtasks, or apply patch-style updates with configurable response size.',
|
|
246
|
+
inputSchema: {
|
|
247
|
+
type: 'object',
|
|
248
|
+
properties: {
|
|
249
|
+
operation: {
|
|
250
|
+
type: 'string',
|
|
251
|
+
enum: ['move', 'toggle', 'update'],
|
|
252
|
+
description: "'move' changes column, 'toggle' flips one subtask, 'update' applies a patch"
|
|
253
|
+
},
|
|
254
|
+
task_id: {
|
|
255
|
+
type: 'string',
|
|
256
|
+
description: 'Task ID to update'
|
|
257
|
+
},
|
|
258
|
+
column: {
|
|
259
|
+
type: 'string',
|
|
260
|
+
enum: COLS,
|
|
261
|
+
description: "New column for 'move'"
|
|
262
|
+
},
|
|
263
|
+
idx: {
|
|
264
|
+
type: 'integer',
|
|
265
|
+
description: "Subtask index for 'toggle'"
|
|
266
|
+
},
|
|
267
|
+
patch: {
|
|
268
|
+
type: 'object',
|
|
269
|
+
description: "Patch payload for 'update'"
|
|
270
|
+
},
|
|
271
|
+
title: {
|
|
272
|
+
type: 'string',
|
|
273
|
+
description: 'Backward-compatible title update shortcut'
|
|
274
|
+
},
|
|
275
|
+
tasks: {
|
|
276
|
+
type: 'array',
|
|
277
|
+
description: 'Backward-compatible subtask update shortcut',
|
|
278
|
+
items: {
|
|
279
|
+
type: 'object',
|
|
280
|
+
properties: {
|
|
281
|
+
id: { type: 'string' },
|
|
282
|
+
done: { type: 'boolean' },
|
|
283
|
+
text: { type: 'string' },
|
|
284
|
+
description: { type: 'string' }
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
return: {
|
|
289
|
+
type: 'string',
|
|
290
|
+
enum: ['none', 'summary', 'full'],
|
|
291
|
+
description: 'Returned payload size after update. Defaults to summary.'
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
required: ['operation', 'task_id']
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
name: 'kanban_gui_start',
|
|
299
|
+
description: 'Start the web GUI server for the kanban board.',
|
|
300
|
+
inputSchema: {
|
|
301
|
+
type: 'object',
|
|
302
|
+
properties: {
|
|
303
|
+
port: {
|
|
304
|
+
type: 'integer',
|
|
305
|
+
description: 'Port for the GUI server (default 5500)'
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
name: 'kanban_gui_stop',
|
|
312
|
+
description: 'Stop the web GUI server if it is running.',
|
|
313
|
+
inputSchema: {
|
|
314
|
+
type: 'object',
|
|
315
|
+
properties: {}
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
name: 'kanban_gui_status',
|
|
320
|
+
description: 'Get status of the web GUI server.',
|
|
321
|
+
inputSchema: {
|
|
322
|
+
type: 'object',
|
|
323
|
+
properties: {}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
]
|
|
327
|
+
};
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
331
|
+
const { name, arguments: args = {} } = request.params;
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
let result;
|
|
335
|
+
|
|
336
|
+
switch (name) {
|
|
337
|
+
case 'kanban_read': {
|
|
338
|
+
const operation = args.operation || 'list';
|
|
339
|
+
const readOptions = normalizeReadOptions(args, 'summary');
|
|
340
|
+
|
|
341
|
+
if (operation === 'list') {
|
|
342
|
+
let tasks = await kanban.allEpics();
|
|
343
|
+
if (args.col) {
|
|
344
|
+
tasks = tasks.filter((task) => task.column === args.col);
|
|
345
|
+
}
|
|
346
|
+
if (args.epic) {
|
|
347
|
+
tasks = tasks.filter((task) => task.epic_group === args.epic);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
result = tasks.map((task) => kanban.shapeTask(task, readOptions));
|
|
351
|
+
} else if (operation === 'show') {
|
|
352
|
+
if (!args.task_id) {
|
|
353
|
+
throw invalidRequest(
|
|
354
|
+
"task_id is required for 'show' operation",
|
|
355
|
+
'Provide the task id you want to inspect',
|
|
356
|
+
{ operation }
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
result = kanban.shapeTask(await kanban.getTask(args.task_id), readOptions);
|
|
361
|
+
} else {
|
|
362
|
+
throw invalidRequest(
|
|
363
|
+
`Unknown operation: ${operation}`,
|
|
364
|
+
'Use one of: list, show',
|
|
365
|
+
{ operation }
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
case 'kanban_create': {
|
|
372
|
+
const created = await kanban.doCreate(args.title, args.col || 'planned', args.epic || '—', {
|
|
373
|
+
description: args.description,
|
|
374
|
+
specs: args.specs,
|
|
375
|
+
acceptance_criteria: args.acceptance_criteria,
|
|
376
|
+
subtasks: args.subtasks,
|
|
377
|
+
notes: args.notes
|
|
378
|
+
});
|
|
379
|
+
result = kanban.shapeTask(created, { view: 'full' });
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
case 'kanban_update': {
|
|
384
|
+
const operation = args.operation;
|
|
385
|
+
const returnShape = normalizeReturnShape(args.return);
|
|
386
|
+
|
|
387
|
+
if (operation === 'move') {
|
|
388
|
+
if (!args.column) {
|
|
389
|
+
throw invalidRequest(
|
|
390
|
+
"column is required for 'move'",
|
|
391
|
+
'Provide one target column',
|
|
392
|
+
{ operation }
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
const updated = await kanban.updateTask(args.task_id, { column: args.column });
|
|
396
|
+
result = formatTaskResult(updated, returnShape);
|
|
397
|
+
} else if (operation === 'toggle') {
|
|
398
|
+
if (!Number.isInteger(args.idx)) {
|
|
399
|
+
throw invalidRequest(
|
|
400
|
+
"idx is required for 'toggle'",
|
|
401
|
+
'Provide a zero-based subtask index',
|
|
402
|
+
{ operation, idx: args.idx }
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
const current = await kanban.getTask(args.task_id);
|
|
406
|
+
if (args.idx < 0 || args.idx >= current.subtasks.length) {
|
|
407
|
+
throw kanban.createKanbanError(
|
|
408
|
+
'INVALID_SUBTASK_INDEX',
|
|
409
|
+
`Subtask index ${args.idx} is not valid for task ${args.task_id}`,
|
|
410
|
+
'Read the task first and use an index between 0 and subtasks.length - 1',
|
|
411
|
+
{ task_id: args.task_id, idx: args.idx, total_subtasks: current.subtasks.length },
|
|
412
|
+
false,
|
|
413
|
+
400
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const subtasks = current.subtasks.map((subtask, idx) => ({
|
|
418
|
+
...subtask,
|
|
419
|
+
done: idx === args.idx ? !subtask.done : subtask.done
|
|
420
|
+
}));
|
|
421
|
+
const updated = await kanban.updateTask(args.task_id, { subtasks });
|
|
422
|
+
result = formatTaskResult(updated, returnShape);
|
|
423
|
+
} else if (operation === 'update') {
|
|
424
|
+
const patch = args.patch ? { ...args.patch } : {};
|
|
425
|
+
if (args.title !== undefined) patch.title = args.title;
|
|
426
|
+
if (args.tasks !== undefined) patch.subtasks = args.tasks;
|
|
427
|
+
const updated = await kanban.updateTask(args.task_id, patch);
|
|
428
|
+
result = formatTaskResult(updated, returnShape);
|
|
429
|
+
} else {
|
|
430
|
+
throw invalidRequest(
|
|
431
|
+
`Unknown operation: ${operation}`,
|
|
432
|
+
'Use one of: move, toggle, update',
|
|
433
|
+
{ operation }
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
case 'kanban_gui_start': {
|
|
440
|
+
result = await startGuiServer(args.port);
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
case 'kanban_gui_stop': {
|
|
445
|
+
result = stopGuiServer();
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
case 'kanban_gui_status': {
|
|
450
|
+
result = guiStatus();
|
|
451
|
+
break;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
default:
|
|
455
|
+
throw invalidRequest(`Unknown tool: ${name}`, 'Call tools/list to discover available tools', { name });
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
content: [
|
|
460
|
+
{
|
|
461
|
+
type: 'text',
|
|
462
|
+
text: typeof result === 'string' ? result : JSON.stringify(result, null, 2)
|
|
463
|
+
}
|
|
464
|
+
]
|
|
465
|
+
};
|
|
466
|
+
} catch (error) {
|
|
467
|
+
return {
|
|
468
|
+
content: [
|
|
469
|
+
{
|
|
470
|
+
type: 'text',
|
|
471
|
+
text: JSON.stringify(serializeError(error), null, 2)
|
|
472
|
+
}
|
|
473
|
+
],
|
|
474
|
+
isError: true
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
async function main() {
|
|
480
|
+
await kanban.ensureBacklogDir();
|
|
481
|
+
const transport = new StdioServerTransport();
|
|
482
|
+
await server.connect(transport);
|
|
483
|
+
console.error('kanbango MCP server running');
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
main().catch((error) => {
|
|
487
|
+
console.error('Fatal error in main():', error);
|
|
488
|
+
process.exit(1);
|
|
489
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kanbango",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "JSON-first local Kanban board with web GUI, CLI, and MCP server",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"kanban": "bin/kanban.js",
|
|
8
|
+
"kanban-cmd": "bin/kanban.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node bin/kanban.js serve",
|
|
12
|
+
"mcp": "node mcp-server.js",
|
|
13
|
+
"test": "node tests/run.js"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"kanban",
|
|
17
|
+
"markdown",
|
|
18
|
+
"task",
|
|
19
|
+
"backlog",
|
|
20
|
+
"cli",
|
|
21
|
+
"local",
|
|
22
|
+
"mcp",
|
|
23
|
+
"model-context-protocol"
|
|
24
|
+
],
|
|
25
|
+
"author": "k0r81",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/k0r81/kanbango.git"
|
|
30
|
+
},
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/k0r81/kanbango/issues"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/k0r81/kanbango#readme",
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=16.0.0"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@modelcontextprotocol/sdk": "^1.0.4"
|
|
40
|
+
}
|
|
41
|
+
}
|