@unisonworkspace/mcp-tasks 1.1.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 +37 -0
- package/dist/client.js +80 -0
- package/dist/formatters.js +104 -0
- package/dist/index.js +24 -0
- package/dist/tools.js +362 -0
- package/package.json +24 -0
package/README.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# @unisonworkspace/mcp-tasks
|
|
2
|
+
|
|
3
|
+
MCP server that connects Claude Code to your Unison workspace. Manage tasks, track status, and add comments — all from your terminal.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
1. Go to **Settings > API Keys** in your Unison workspace and create an API key
|
|
8
|
+
2. Run this in your terminal:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
claude mcp add --transport stdio \
|
|
12
|
+
-e UNISON_API_TOKEN=sk-your-key \
|
|
13
|
+
-e UNISON_API_URL=https://app.unisonworkspace.com \
|
|
14
|
+
unison-tasks -- npx -y @unisonworkspace/mcp-tasks
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
3. Restart Claude Code
|
|
18
|
+
|
|
19
|
+
## Available Tools
|
|
20
|
+
|
|
21
|
+
| Tool | Description |
|
|
22
|
+
|------|-------------|
|
|
23
|
+
| `list_space_schema` | Get a team space's statuses, task type, and custom fields |
|
|
24
|
+
| `list_tasks` | List tasks with optional filters (status, priority, assignee) |
|
|
25
|
+
| `get_task` | Get a single task with its comments |
|
|
26
|
+
| `create_task` | Create a new task |
|
|
27
|
+
| `update_task` | Update an existing task |
|
|
28
|
+
| `delete_task` | Delete a task |
|
|
29
|
+
| `add_comment` | Add a comment to a task |
|
|
30
|
+
| `move_task` | Move a task to a different status column |
|
|
31
|
+
|
|
32
|
+
## Environment Variables
|
|
33
|
+
|
|
34
|
+
| Variable | Description |
|
|
35
|
+
|----------|-------------|
|
|
36
|
+
| `UNISON_API_TOKEN` | Your Unison API key (required) |
|
|
37
|
+
| `UNISON_API_URL` | Your Unison workspace URL (required) |
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.UnisonClient = exports.ApiError = void 0;
|
|
4
|
+
const REQUEST_TIMEOUT_MS = 30000;
|
|
5
|
+
class ApiError extends Error {
|
|
6
|
+
constructor(status, message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.name = 'ApiError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
exports.ApiError = ApiError;
|
|
13
|
+
class UnisonClient {
|
|
14
|
+
constructor() {
|
|
15
|
+
const token = process.env.UNISON_API_TOKEN;
|
|
16
|
+
if (!token) {
|
|
17
|
+
console.error('[unison-tasks] UNISON_API_TOKEN environment variable is required');
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
this.token = token;
|
|
21
|
+
this.baseUrl = (process.env.UNISON_API_URL || 'http://localhost:3200').replace(/\/$/, '');
|
|
22
|
+
}
|
|
23
|
+
async request(method, path, body, query) {
|
|
24
|
+
let url = `${this.baseUrl}${path}`;
|
|
25
|
+
if (query) {
|
|
26
|
+
const params = new URLSearchParams();
|
|
27
|
+
for (const [key, value] of Object.entries(query)) {
|
|
28
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
29
|
+
params.set(key, value);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const qs = params.toString();
|
|
33
|
+
if (qs)
|
|
34
|
+
url += `?${qs}`;
|
|
35
|
+
}
|
|
36
|
+
let res;
|
|
37
|
+
try {
|
|
38
|
+
res = await fetch(url, {
|
|
39
|
+
method,
|
|
40
|
+
headers: {
|
|
41
|
+
'Content-Type': 'application/json',
|
|
42
|
+
'Authorization': `Bearer ${this.token}`,
|
|
43
|
+
},
|
|
44
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
45
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
if (err instanceof Error && err.name === 'TimeoutError') {
|
|
50
|
+
throw new ApiError(0, `Request timed out after ${REQUEST_TIMEOUT_MS / 1000}s: ${method} ${path}`);
|
|
51
|
+
}
|
|
52
|
+
throw new ApiError(0, `Cannot reach Unison API at ${this.baseUrl}`);
|
|
53
|
+
}
|
|
54
|
+
if (!res.ok) {
|
|
55
|
+
let message;
|
|
56
|
+
try {
|
|
57
|
+
const json = await res.json();
|
|
58
|
+
message = json.error || json.message || res.statusText;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
message = res.statusText;
|
|
62
|
+
}
|
|
63
|
+
throw new ApiError(res.status, message);
|
|
64
|
+
}
|
|
65
|
+
return res.json();
|
|
66
|
+
}
|
|
67
|
+
async get(path, query) {
|
|
68
|
+
return this.request('GET', path, undefined, query);
|
|
69
|
+
}
|
|
70
|
+
async post(path, body) {
|
|
71
|
+
return this.request('POST', path, body);
|
|
72
|
+
}
|
|
73
|
+
async put(path, body) {
|
|
74
|
+
return this.request('PUT', path, body);
|
|
75
|
+
}
|
|
76
|
+
async delete(path) {
|
|
77
|
+
return this.request('DELETE', path);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
exports.UnisonClient = UnisonClient;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatTaskList = formatTaskList;
|
|
4
|
+
exports.formatTaskDetail = formatTaskDetail;
|
|
5
|
+
exports.formatCreated = formatCreated;
|
|
6
|
+
exports.formatUpdated = formatUpdated;
|
|
7
|
+
exports.formatDeleted = formatDeleted;
|
|
8
|
+
exports.formatCommentAdded = formatCommentAdded;
|
|
9
|
+
exports.formatMoved = formatMoved;
|
|
10
|
+
const PRIORITY_BADGE = {
|
|
11
|
+
urgent: '[!!!]',
|
|
12
|
+
high: '[!!]',
|
|
13
|
+
medium: '[!]',
|
|
14
|
+
low: '[-]',
|
|
15
|
+
none: '[ ]',
|
|
16
|
+
};
|
|
17
|
+
function priorityBadge(p) {
|
|
18
|
+
return PRIORITY_BADGE[p] || `[${p}]`;
|
|
19
|
+
}
|
|
20
|
+
function formatDate(d) {
|
|
21
|
+
if (!d)
|
|
22
|
+
return '';
|
|
23
|
+
return new Date(d).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
|
24
|
+
}
|
|
25
|
+
function formatTaskList(tasks) {
|
|
26
|
+
if (tasks.length === 0)
|
|
27
|
+
return 'No tasks found.';
|
|
28
|
+
const grouped = {};
|
|
29
|
+
for (const t of tasks) {
|
|
30
|
+
const status = t.status || 'unknown';
|
|
31
|
+
if (!grouped[status])
|
|
32
|
+
grouped[status] = [];
|
|
33
|
+
grouped[status].push(t);
|
|
34
|
+
}
|
|
35
|
+
const statusOrder = ['todo', 'in_progress', 'review', 'done'];
|
|
36
|
+
const sortedStatuses = Object.keys(grouped).sort((a, b) => {
|
|
37
|
+
const ai = statusOrder.indexOf(a);
|
|
38
|
+
const bi = statusOrder.indexOf(b);
|
|
39
|
+
return (ai === -1 ? 99 : ai) - (bi === -1 ? 99 : bi);
|
|
40
|
+
});
|
|
41
|
+
const lines = [`${tasks.length} task(s)\n`];
|
|
42
|
+
for (const status of sortedStatuses) {
|
|
43
|
+
lines.push(`── ${status.toUpperCase().replace('_', ' ')} (${grouped[status].length}) ──`);
|
|
44
|
+
for (const t of grouped[status]) {
|
|
45
|
+
const assignee = t.assigneeName ? ` → ${t.assigneeName}` : '';
|
|
46
|
+
const project = t.projectName ? ` [${t.projectName}]` : '';
|
|
47
|
+
const due = t.dueDate ? ` due ${formatDate(t.dueDate)}` : '';
|
|
48
|
+
lines.push(` ${priorityBadge(t.priority)} ${t.title}${assignee}${project}${due}`);
|
|
49
|
+
lines.push(` id: ${t.id}`);
|
|
50
|
+
}
|
|
51
|
+
lines.push('');
|
|
52
|
+
}
|
|
53
|
+
return lines.join('\n');
|
|
54
|
+
}
|
|
55
|
+
function formatTaskDetail(task, comments) {
|
|
56
|
+
const lines = [
|
|
57
|
+
`# ${task.title}`,
|
|
58
|
+
`ID: ${task.id}`,
|
|
59
|
+
`Status: ${task.status} Priority: ${task.priority}`,
|
|
60
|
+
];
|
|
61
|
+
if (task.assigneeName)
|
|
62
|
+
lines.push(`Assignee: ${task.assigneeName} (${task.assigneeType})`);
|
|
63
|
+
if (task.projectName)
|
|
64
|
+
lines.push(`Project: ${task.projectName}`);
|
|
65
|
+
if (task.dueDate)
|
|
66
|
+
lines.push(`Due: ${formatDate(task.dueDate)}`);
|
|
67
|
+
if (task.description)
|
|
68
|
+
lines.push(`\nDescription:\n${task.description}`);
|
|
69
|
+
if (task.fieldValues && Object.keys(task.fieldValues).length > 0) {
|
|
70
|
+
lines.push(`\n── Fields ──`);
|
|
71
|
+
for (const fv of Object.values(task.fieldValues)) {
|
|
72
|
+
const displayValue = fv.value != null && fv.value !== '' && fv.value !== '""' ? String(fv.value).replace(/^"|"$/g, '') : '(empty)';
|
|
73
|
+
lines.push(` ${fv.fieldName}: ${displayValue}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (task.createdAt)
|
|
77
|
+
lines.push(`\nCreated: ${formatDate(task.createdAt)}`);
|
|
78
|
+
if (task.updatedAt)
|
|
79
|
+
lines.push(`Updated: ${formatDate(task.updatedAt)}`);
|
|
80
|
+
if (comments && comments.length > 0) {
|
|
81
|
+
lines.push(`\n── Comments (${comments.length}) ──`);
|
|
82
|
+
for (const c of comments) {
|
|
83
|
+
lines.push(` ${c.authorName || 'Unknown'} (${formatDate(c.createdAt)}):`);
|
|
84
|
+
lines.push(` ${c.content}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return lines.join('\n');
|
|
88
|
+
}
|
|
89
|
+
function formatCreated(task) {
|
|
90
|
+
return `Task created: "${task.title}" (${task.id})\nStatus: ${task.status} Priority: ${task.priority}`;
|
|
91
|
+
}
|
|
92
|
+
function formatUpdated(task) {
|
|
93
|
+
return `Task updated: "${task.title}" (${task.id})\nStatus: ${task.status} Priority: ${task.priority}`;
|
|
94
|
+
}
|
|
95
|
+
function formatDeleted(taskId) {
|
|
96
|
+
return `Task deleted: ${taskId}`;
|
|
97
|
+
}
|
|
98
|
+
function formatCommentAdded(taskId) {
|
|
99
|
+
return `Comment added to task ${taskId}`;
|
|
100
|
+
}
|
|
101
|
+
function formatMoved(taskId, status) {
|
|
102
|
+
const statusMsg = status ? ` to ${status}` : '';
|
|
103
|
+
return `Task ${taskId} moved${statusMsg}`;
|
|
104
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
5
|
+
const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
7
|
+
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
|
|
8
|
+
const client_1 = require("./client");
|
|
9
|
+
const tools_1 = require("./tools");
|
|
10
|
+
async function main() {
|
|
11
|
+
const client = new client_1.UnisonClient();
|
|
12
|
+
const server = new McpServer({
|
|
13
|
+
name: 'unison-tasks',
|
|
14
|
+
version: '1.0.0',
|
|
15
|
+
});
|
|
16
|
+
(0, tools_1.registerTools)(server, client);
|
|
17
|
+
const transport = new StdioServerTransport();
|
|
18
|
+
await server.connect(transport);
|
|
19
|
+
console.error('[unison-tasks] MCP server started');
|
|
20
|
+
}
|
|
21
|
+
main().catch((err) => {
|
|
22
|
+
console.error('[unison-tasks] Fatal error:', err);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
});
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerTools = registerTools;
|
|
4
|
+
const client_1 = require("./client");
|
|
5
|
+
const formatters_1 = require("./formatters");
|
|
6
|
+
function errorResponse(err) {
|
|
7
|
+
if (err instanceof client_1.ApiError) {
|
|
8
|
+
const messages = {
|
|
9
|
+
401: 'Authentication failed. Check your UNISON_API_TOKEN.',
|
|
10
|
+
403: 'Permission denied.',
|
|
11
|
+
404: 'Not found.',
|
|
12
|
+
};
|
|
13
|
+
const text = messages[err.status] || (err.status === 400 ? err.message : `API error (${err.status}): ${err.message}`);
|
|
14
|
+
return { content: [{ type: 'text', text }], isError: true };
|
|
15
|
+
}
|
|
16
|
+
if (err instanceof Error) {
|
|
17
|
+
return { content: [{ type: 'text', text: err.message }], isError: true };
|
|
18
|
+
}
|
|
19
|
+
return { content: [{ type: 'text', text: `Unexpected error: ${String(err)}` }], isError: true };
|
|
20
|
+
}
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
22
|
+
function ok(text) {
|
|
23
|
+
return { content: [{ type: 'text', text }] };
|
|
24
|
+
}
|
|
25
|
+
function formatSchema(schema) {
|
|
26
|
+
const lines = [];
|
|
27
|
+
const taskType = schema.taskType;
|
|
28
|
+
if (taskType) {
|
|
29
|
+
lines.push(`Type: ${taskType.name} (${taskType.slug})`);
|
|
30
|
+
}
|
|
31
|
+
lines.push(`Default view: ${schema.defaultView || 'board'}`);
|
|
32
|
+
const columns = schema.kanbanColumns;
|
|
33
|
+
if (columns && columns.length > 0) {
|
|
34
|
+
lines.push(`\nStatuses: ${columns.map(c => `${c.label} (${c.id})`).join(' → ')}`);
|
|
35
|
+
}
|
|
36
|
+
const fields = taskType?.fields;
|
|
37
|
+
if (fields && fields.length > 0) {
|
|
38
|
+
lines.push(`\nCustom fields:`);
|
|
39
|
+
for (const f of fields) {
|
|
40
|
+
const req = f.isRequired ? ' (required)' : '';
|
|
41
|
+
const opts = f.config?.options ? ` [${f.config.options.join(', ')}]` : '';
|
|
42
|
+
lines.push(` - ${f.name} (${f.fieldType})${req}${opts}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return lines.join('\n');
|
|
46
|
+
}
|
|
47
|
+
function registerTools(server, client) {
|
|
48
|
+
// ── list_spaces ──
|
|
49
|
+
server.tool('list_spaces', {
|
|
50
|
+
description: 'List all team spaces you have access to. Returns space names and IDs. Use this to discover space UUIDs before creating tasks or querying schemas.',
|
|
51
|
+
inputSchema: {
|
|
52
|
+
type: 'object',
|
|
53
|
+
properties: {},
|
|
54
|
+
},
|
|
55
|
+
}, async () => {
|
|
56
|
+
try {
|
|
57
|
+
const res = (await client.get('/api/team-spaces'));
|
|
58
|
+
const spaces = res.data;
|
|
59
|
+
if (spaces.length === 0)
|
|
60
|
+
return ok('No team spaces found.');
|
|
61
|
+
const lines = [`${spaces.length} team space(s)\n`];
|
|
62
|
+
for (const s of spaces) {
|
|
63
|
+
const desc = s.description ? ` — ${s.description}` : '';
|
|
64
|
+
lines.push(` ${s.name}${desc}`);
|
|
65
|
+
lines.push(` id: ${s.id}`);
|
|
66
|
+
}
|
|
67
|
+
return ok(lines.join('\n'));
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
return errorResponse(err);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
// ── list_projects ──
|
|
74
|
+
server.tool('list_projects', {
|
|
75
|
+
description: 'List all projects you have access to. Returns project names, IDs, and task counts. Use this to discover project UUIDs.',
|
|
76
|
+
inputSchema: {
|
|
77
|
+
type: 'object',
|
|
78
|
+
properties: {
|
|
79
|
+
team_space_id: { type: 'string', description: 'Filter by team space UUID' },
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
}, async (params) => {
|
|
83
|
+
try {
|
|
84
|
+
const query = {};
|
|
85
|
+
if (params.team_space_id)
|
|
86
|
+
query.teamSpaceId = params.team_space_id;
|
|
87
|
+
const res = (await client.get('/api/projects', query));
|
|
88
|
+
const projects = res.data;
|
|
89
|
+
if (projects.length === 0)
|
|
90
|
+
return ok('No projects found.');
|
|
91
|
+
const lines = [`${projects.length} project(s)\n`];
|
|
92
|
+
for (const p of projects) {
|
|
93
|
+
const status = p.status ? ` (${p.status})` : '';
|
|
94
|
+
const space = p.teamSpaceName ? ` [${p.teamSpaceName}]` : '';
|
|
95
|
+
const tasks = p.taskCount !== undefined ? ` — ${p.taskCount} tasks` : '';
|
|
96
|
+
lines.push(` ${p.name}${status}${space}${tasks}`);
|
|
97
|
+
lines.push(` id: ${p.id}`);
|
|
98
|
+
}
|
|
99
|
+
return ok(lines.join('\n'));
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
return errorResponse(err);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
// ── list_space_schema ──
|
|
106
|
+
server.tool('list_space_schema', {
|
|
107
|
+
description: 'Get a team space\'s schema: its statuses (kanban columns), task type, and custom fields. Use before creating tasks to know valid statuses and available fields.',
|
|
108
|
+
inputSchema: {
|
|
109
|
+
type: 'object',
|
|
110
|
+
properties: {
|
|
111
|
+
team_space_id: { type: 'string', description: 'Team space UUID' },
|
|
112
|
+
},
|
|
113
|
+
required: ['team_space_id'],
|
|
114
|
+
},
|
|
115
|
+
}, async (params) => {
|
|
116
|
+
try {
|
|
117
|
+
const spaceId = params.team_space_id;
|
|
118
|
+
const res = (await client.get(`/api/team-spaces/${spaceId}/schema`));
|
|
119
|
+
return ok(formatSchema(res.data));
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
return errorResponse(err);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
// ── list_project_schema ──
|
|
126
|
+
server.tool('list_project_schema', {
|
|
127
|
+
description: 'Get a project\'s schema: its statuses (kanban columns), task type, and custom fields. Use before creating tasks in a project to know valid statuses and available fields. Falls back to the project\'s team space schema if the project has no own type.',
|
|
128
|
+
inputSchema: {
|
|
129
|
+
type: 'object',
|
|
130
|
+
properties: {
|
|
131
|
+
project_id: { type: 'string', description: 'Project UUID' },
|
|
132
|
+
},
|
|
133
|
+
required: ['project_id'],
|
|
134
|
+
},
|
|
135
|
+
}, async (params) => {
|
|
136
|
+
try {
|
|
137
|
+
const projectId = params.project_id;
|
|
138
|
+
const res = (await client.get(`/api/projects/${projectId}/schema`));
|
|
139
|
+
return ok(formatSchema(res.data));
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
return errorResponse(err);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
// ── list_tasks ──
|
|
146
|
+
server.tool('list_tasks', {
|
|
147
|
+
description: 'List tasks with optional filters. Returns all tasks across projects. Use list_space_schema first to discover valid statuses for a space.',
|
|
148
|
+
inputSchema: {
|
|
149
|
+
type: 'object',
|
|
150
|
+
properties: {
|
|
151
|
+
status: { type: 'string', description: 'Filter by status (e.g. todo, in_progress, review, done, or any custom status from the space)' },
|
|
152
|
+
priority: { type: 'string', enum: ['urgent', 'high', 'medium', 'low', 'none'], description: 'Filter by priority' },
|
|
153
|
+
assignee_id: { type: 'string', description: 'Filter by assignee UUID' },
|
|
154
|
+
my_tasks: { type: 'boolean', description: 'Show only tasks assigned to the authenticated user' },
|
|
155
|
+
team_space_id: { type: 'string', description: 'Filter by team space UUID' },
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
}, async (params) => {
|
|
159
|
+
try {
|
|
160
|
+
const query = {};
|
|
161
|
+
if (params.status)
|
|
162
|
+
query.status = params.status;
|
|
163
|
+
if (params.priority)
|
|
164
|
+
query.priority = params.priority;
|
|
165
|
+
if (params.assignee_id)
|
|
166
|
+
query.assignee_id = params.assignee_id;
|
|
167
|
+
if (params.my_tasks)
|
|
168
|
+
query.my_tasks = 'true';
|
|
169
|
+
if (params.team_space_id)
|
|
170
|
+
query.teamSpaceId = params.team_space_id;
|
|
171
|
+
const res = (await client.get('/api/projects/tasks', query));
|
|
172
|
+
return ok((0, formatters_1.formatTaskList)(res.data));
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
return errorResponse(err);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
// ── get_task ──
|
|
179
|
+
server.tool('get_task', {
|
|
180
|
+
description: 'Get a single task by ID, including its comments.',
|
|
181
|
+
inputSchema: {
|
|
182
|
+
type: 'object',
|
|
183
|
+
properties: {
|
|
184
|
+
task_id: { type: 'string', description: 'Task UUID' },
|
|
185
|
+
},
|
|
186
|
+
required: ['task_id'],
|
|
187
|
+
},
|
|
188
|
+
}, async (params) => {
|
|
189
|
+
try {
|
|
190
|
+
const taskId = params.task_id;
|
|
191
|
+
const [taskRes, commentsRes] = await Promise.all([
|
|
192
|
+
client.get(`/api/projects/tasks/${taskId}`),
|
|
193
|
+
client.get(`/api/projects/tasks/${taskId}/comments`),
|
|
194
|
+
]);
|
|
195
|
+
return ok((0, formatters_1.formatTaskDetail)(taskRes.data, commentsRes.data));
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
return errorResponse(err);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
// ── create_task ──
|
|
202
|
+
server.tool('create_task', {
|
|
203
|
+
description: 'Create a new task. Only title is required. Use list_space_schema to discover valid statuses and custom fields for a space.',
|
|
204
|
+
inputSchema: {
|
|
205
|
+
type: 'object',
|
|
206
|
+
properties: {
|
|
207
|
+
title: { type: 'string', description: 'Task title' },
|
|
208
|
+
description: { type: 'string', description: 'Task description' },
|
|
209
|
+
status: { type: 'string', description: 'Initial status (default: first status in space schema, e.g. todo)' },
|
|
210
|
+
priority: { type: 'string', enum: ['urgent', 'high', 'medium', 'low', 'none'], description: 'Priority (default: medium)' },
|
|
211
|
+
assignee_id: { type: 'string', description: 'Assignee UUID' },
|
|
212
|
+
assignee_type: { type: 'string', enum: ['user', 'agent'], description: 'Assignee type (default: user)' },
|
|
213
|
+
due_date: { type: 'string', description: 'Due date (ISO 8601)' },
|
|
214
|
+
project_id: { type: 'string', description: 'Project UUID' },
|
|
215
|
+
team_space_id: { type: 'string', description: 'Team space UUID' },
|
|
216
|
+
custom_fields: { type: 'object', description: 'Custom field values as { fieldId: value } pairs. Get field IDs from list_space_schema.' },
|
|
217
|
+
},
|
|
218
|
+
required: ['title'],
|
|
219
|
+
},
|
|
220
|
+
}, async (params) => {
|
|
221
|
+
try {
|
|
222
|
+
const body = { title: params.title };
|
|
223
|
+
if (params.description !== undefined)
|
|
224
|
+
body.description = params.description;
|
|
225
|
+
if (params.status !== undefined)
|
|
226
|
+
body.status = params.status;
|
|
227
|
+
if (params.priority !== undefined)
|
|
228
|
+
body.priority = params.priority;
|
|
229
|
+
if (params.assignee_id !== undefined)
|
|
230
|
+
body.assignee_id = params.assignee_id;
|
|
231
|
+
if (params.assignee_type !== undefined)
|
|
232
|
+
body.assignee_type = params.assignee_type;
|
|
233
|
+
if (params.due_date !== undefined)
|
|
234
|
+
body.due_date = params.due_date;
|
|
235
|
+
if (params.project_id !== undefined)
|
|
236
|
+
body.project_id = params.project_id;
|
|
237
|
+
if (params.team_space_id !== undefined)
|
|
238
|
+
body.team_space_id = params.team_space_id;
|
|
239
|
+
if (params.custom_fields !== undefined)
|
|
240
|
+
body.fieldValues = params.custom_fields;
|
|
241
|
+
const res = (await client.post('/api/projects/tasks', body));
|
|
242
|
+
return ok((0, formatters_1.formatCreated)(res.data));
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
return errorResponse(err);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
// ── update_task ──
|
|
249
|
+
server.tool('update_task', {
|
|
250
|
+
description: 'Update an existing task. Only task_id is required; include only fields to change. Use list_space_schema to discover valid statuses.',
|
|
251
|
+
inputSchema: {
|
|
252
|
+
type: 'object',
|
|
253
|
+
properties: {
|
|
254
|
+
task_id: { type: 'string', description: 'Task UUID' },
|
|
255
|
+
title: { type: 'string', description: 'New title' },
|
|
256
|
+
description: { type: 'string', description: 'New description' },
|
|
257
|
+
status: { type: 'string', description: 'New status (use list_space_schema to discover valid statuses)' },
|
|
258
|
+
priority: { type: 'string', enum: ['urgent', 'high', 'medium', 'low', 'none'], description: 'New priority' },
|
|
259
|
+
assignee_id: { type: 'string', description: 'New assignee UUID' },
|
|
260
|
+
assignee_type: { type: 'string', enum: ['user', 'agent'], description: 'Assignee type' },
|
|
261
|
+
due_date: { type: 'string', description: 'New due date (ISO 8601)' },
|
|
262
|
+
custom_fields: { type: 'object', description: 'Custom field values as { fieldId: value } pairs' },
|
|
263
|
+
},
|
|
264
|
+
required: ['task_id'],
|
|
265
|
+
},
|
|
266
|
+
}, async (params) => {
|
|
267
|
+
try {
|
|
268
|
+
const taskId = params.task_id;
|
|
269
|
+
const body = {};
|
|
270
|
+
if (params.title !== undefined)
|
|
271
|
+
body.title = params.title;
|
|
272
|
+
if (params.description !== undefined)
|
|
273
|
+
body.description = params.description;
|
|
274
|
+
if (params.status !== undefined)
|
|
275
|
+
body.status = params.status;
|
|
276
|
+
if (params.priority !== undefined)
|
|
277
|
+
body.priority = params.priority;
|
|
278
|
+
if (params.assignee_id !== undefined)
|
|
279
|
+
body.assignee_id = params.assignee_id;
|
|
280
|
+
if (params.assignee_type !== undefined)
|
|
281
|
+
body.assignee_type = params.assignee_type;
|
|
282
|
+
if (params.due_date !== undefined)
|
|
283
|
+
body.due_date = params.due_date;
|
|
284
|
+
if (params.custom_fields !== undefined)
|
|
285
|
+
body.fieldValues = params.custom_fields;
|
|
286
|
+
const res = (await client.put(`/api/projects/tasks/${taskId}`, body));
|
|
287
|
+
return ok((0, formatters_1.formatUpdated)(res.data));
|
|
288
|
+
}
|
|
289
|
+
catch (err) {
|
|
290
|
+
return errorResponse(err);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
// ── delete_task ──
|
|
294
|
+
server.tool('delete_task', {
|
|
295
|
+
description: 'Delete a task permanently.',
|
|
296
|
+
inputSchema: {
|
|
297
|
+
type: 'object',
|
|
298
|
+
properties: {
|
|
299
|
+
task_id: { type: 'string', description: 'Task UUID' },
|
|
300
|
+
},
|
|
301
|
+
required: ['task_id'],
|
|
302
|
+
},
|
|
303
|
+
}, async (params) => {
|
|
304
|
+
try {
|
|
305
|
+
const taskId = params.task_id;
|
|
306
|
+
await client.delete(`/api/projects/tasks/${taskId}`);
|
|
307
|
+
return ok((0, formatters_1.formatDeleted)(taskId));
|
|
308
|
+
}
|
|
309
|
+
catch (err) {
|
|
310
|
+
return errorResponse(err);
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
// ── add_comment ──
|
|
314
|
+
server.tool('add_comment', {
|
|
315
|
+
description: 'Add a comment to a task.',
|
|
316
|
+
inputSchema: {
|
|
317
|
+
type: 'object',
|
|
318
|
+
properties: {
|
|
319
|
+
task_id: { type: 'string', description: 'Task UUID' },
|
|
320
|
+
content: { type: 'string', description: 'Comment text' },
|
|
321
|
+
},
|
|
322
|
+
required: ['task_id', 'content'],
|
|
323
|
+
},
|
|
324
|
+
}, async (params) => {
|
|
325
|
+
try {
|
|
326
|
+
const taskId = params.task_id;
|
|
327
|
+
await client.post(`/api/projects/tasks/${taskId}/comments`, { content: params.content });
|
|
328
|
+
return ok((0, formatters_1.formatCommentAdded)(taskId));
|
|
329
|
+
}
|
|
330
|
+
catch (err) {
|
|
331
|
+
return errorResponse(err);
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
// ── move_task ──
|
|
335
|
+
server.tool('move_task', {
|
|
336
|
+
description: 'Move a task to a different status column and/or reorder within a column.',
|
|
337
|
+
inputSchema: {
|
|
338
|
+
type: 'object',
|
|
339
|
+
properties: {
|
|
340
|
+
task_id: { type: 'string', description: 'Task UUID' },
|
|
341
|
+
status: { type: 'string', description: 'Target status column (use list_space_schema to discover valid statuses)' },
|
|
342
|
+
position: { type: 'number', description: 'Zero-based index in the target column (default: 0 = top)' },
|
|
343
|
+
},
|
|
344
|
+
required: ['task_id'],
|
|
345
|
+
},
|
|
346
|
+
}, async (params) => {
|
|
347
|
+
try {
|
|
348
|
+
const taskId = params.task_id;
|
|
349
|
+
const body = {
|
|
350
|
+
taskId,
|
|
351
|
+
newIndex: params.position !== undefined ? params.position : 0,
|
|
352
|
+
};
|
|
353
|
+
if (params.status !== undefined)
|
|
354
|
+
body.status = params.status;
|
|
355
|
+
await client.put('/api/projects/tasks/reorder', body);
|
|
356
|
+
return ok((0, formatters_1.formatMoved)(taskId, params.status));
|
|
357
|
+
}
|
|
358
|
+
catch (err) {
|
|
359
|
+
return errorResponse(err);
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@unisonworkspace/mcp-tasks",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Unison Tasks MCP server — exposes task CRUD via Model Context Protocol",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mcp-tasks": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist/"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"start": "node dist/index.js",
|
|
15
|
+
"dev": "ts-node src/index.ts"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@modelcontextprotocol/sdk": "^1.12.1"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"typescript": "^5.5.0",
|
|
22
|
+
"ts-node": "^10.9.2"
|
|
23
|
+
}
|
|
24
|
+
}
|