@knotsystems/task-overflow-mcp 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 +60 -0
- package/package.json +26 -0
- package/src/index.js +272 -0
package/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# task-overflow-mcp
|
|
2
|
+
|
|
3
|
+
An MCP (Model Context Protocol) server for **Claude Code** that connects to the **Task Overflow** Mac app. Use Claude to list projects, manage tasks, tags, and subgroups through natural language.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- **Task Overflow** Mac app installed and running
|
|
8
|
+
- **Node.js** installed (v18+)
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
Install globally via npm:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install -g task-overflow-mcp
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Or for local development, install from the checked-out repository:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
cd task-overflow-mcp
|
|
22
|
+
npm install -g .
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Configuration
|
|
26
|
+
|
|
27
|
+
Add the server to Claude Code with the MCP add command. You must set `TASK_OVERFLOW_TOKEN` so the server can authenticate with the Task Overflow app.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
claude mcp add task-overflow-mcp --command "task-overflow-mcp" --env "TASK_OVERFLOW_TOKEN=your_token_here"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or configure via your Claude Code MCP settings (e.g. `~/.claude/claude_mcp_settings.json` or the equivalent in your setup) with the command and environment variable.
|
|
34
|
+
|
|
35
|
+
### Where to find the token
|
|
36
|
+
|
|
37
|
+
In the **Task Overflow** app, open **Settings** (⌘,) and copy the API token shown there. Use that value for `TASK_OVERFLOW_TOKEN`.
|
|
38
|
+
|
|
39
|
+
## Available tools
|
|
40
|
+
|
|
41
|
+
| Tool | Description |
|
|
42
|
+
|------|-------------|
|
|
43
|
+
| `list_projects` | List all projects |
|
|
44
|
+
| `get_project_tasks` | Get all tasks for a project |
|
|
45
|
+
| `get_project_subgroups` | Get all subgroups for a project |
|
|
46
|
+
| `get_project_tags` | Get all tags for a project |
|
|
47
|
+
| `get_task` | Get a single task by ID |
|
|
48
|
+
| `create_task` | Create a new task |
|
|
49
|
+
| `update_task` | Update a task |
|
|
50
|
+
| `complete_task` | Mark a task as complete |
|
|
51
|
+
| `add_tag_to_task` | Add a tag to a task |
|
|
52
|
+
| `remove_tag_from_task` | Remove a tag from a task |
|
|
53
|
+
|
|
54
|
+
## How the API works
|
|
55
|
+
|
|
56
|
+
The MCP server talks to Task Overflow over **local HTTP on port 8491**. All requests use **Bearer token** authentication with your `TASK_OVERFLOW_TOKEN`. The app exposes a REST-style API that the server calls when you use the tools from Claude.
|
|
57
|
+
|
|
58
|
+
## Note
|
|
59
|
+
|
|
60
|
+
**The Task Overflow app must be running** for the MCP server to work. If the app is quit or not listening on port 8491, tool calls will fail.
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@knotsystems/task-overflow-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "An MCP server for the Task Overflow Mac app",
|
|
5
|
+
"author": "Don Parkinson",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"mcp",
|
|
9
|
+
"modelcontextprotocol",
|
|
10
|
+
"task-overflow",
|
|
11
|
+
"claude"
|
|
12
|
+
],
|
|
13
|
+
"type": "module",
|
|
14
|
+
"bin": {
|
|
15
|
+
"task-overflow-mcp": "src/index.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"src"
|
|
19
|
+
],
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@modelcontextprotocol/sdk": "latest"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"start": "node src/index.js"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
|
|
6
|
+
const TASK_OVERFLOW_TOKEN = process.env.TASK_OVERFLOW_TOKEN;
|
|
7
|
+
if (!TASK_OVERFLOW_TOKEN) {
|
|
8
|
+
console.error("task-overflow-mcp: TASK_OVERFLOW_TOKEN is required");
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const BASE_URL = "http://localhost:8491";
|
|
13
|
+
|
|
14
|
+
async function apiCall(method, path, body) {
|
|
15
|
+
const url = BASE_URL + path;
|
|
16
|
+
const options = {
|
|
17
|
+
method,
|
|
18
|
+
headers: {
|
|
19
|
+
Authorization: `Bearer ${TASK_OVERFLOW_TOKEN}`,
|
|
20
|
+
"Content-Type": "application/json",
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
if (body !== undefined) {
|
|
24
|
+
options.body = JSON.stringify(body);
|
|
25
|
+
}
|
|
26
|
+
const response = await fetch(url, options);
|
|
27
|
+
if (!response.ok) {
|
|
28
|
+
const text = await response.text();
|
|
29
|
+
throw new Error(`API ${method} ${path}: ${response.status} ${response.statusText} - ${text}`);
|
|
30
|
+
}
|
|
31
|
+
return response.json();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const server = new Server(
|
|
35
|
+
{
|
|
36
|
+
name: "task-overflow",
|
|
37
|
+
version: "1.0.0",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
capabilities: {
|
|
41
|
+
tools: {},
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const TOOLS = [
|
|
47
|
+
{
|
|
48
|
+
name: "list_projects",
|
|
49
|
+
description: "List all projects",
|
|
50
|
+
inputSchema: {
|
|
51
|
+
type: "object",
|
|
52
|
+
properties: {},
|
|
53
|
+
required: [],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: "get_project_tasks",
|
|
58
|
+
description: "Get all tasks for a project",
|
|
59
|
+
inputSchema: {
|
|
60
|
+
type: "object",
|
|
61
|
+
properties: {
|
|
62
|
+
projectId: { type: "string", description: "Project ID" },
|
|
63
|
+
},
|
|
64
|
+
required: ["projectId"],
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: "get_project_subgroups",
|
|
69
|
+
description: "Get all subgroups for a project",
|
|
70
|
+
inputSchema: {
|
|
71
|
+
type: "object",
|
|
72
|
+
properties: {
|
|
73
|
+
projectId: { type: "string", description: "Project ID" },
|
|
74
|
+
},
|
|
75
|
+
required: ["projectId"],
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: "get_project_tags",
|
|
80
|
+
description: "Get all tags for a project",
|
|
81
|
+
inputSchema: {
|
|
82
|
+
type: "object",
|
|
83
|
+
properties: {
|
|
84
|
+
projectId: { type: "string", description: "Project ID" },
|
|
85
|
+
},
|
|
86
|
+
required: ["projectId"],
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: "get_task",
|
|
91
|
+
description: "Get a single task by ID",
|
|
92
|
+
inputSchema: {
|
|
93
|
+
type: "object",
|
|
94
|
+
properties: {
|
|
95
|
+
taskId: { type: "string", description: "Task ID" },
|
|
96
|
+
},
|
|
97
|
+
required: ["taskId"],
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: "create_task",
|
|
102
|
+
description: "Create a new task",
|
|
103
|
+
inputSchema: {
|
|
104
|
+
type: "object",
|
|
105
|
+
properties: {
|
|
106
|
+
projectId: { type: "string", description: "Project ID" },
|
|
107
|
+
name: { type: "string", description: "Task name" },
|
|
108
|
+
taskDescription: { type: "string", description: "Task description" },
|
|
109
|
+
priority: { type: "number", description: "Priority" },
|
|
110
|
+
status: { type: "string", description: "Status" },
|
|
111
|
+
subgroupId: { type: "string", description: "Subgroup ID" },
|
|
112
|
+
},
|
|
113
|
+
required: ["projectId", "name"],
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: "update_task",
|
|
118
|
+
description: "Update a task",
|
|
119
|
+
inputSchema: {
|
|
120
|
+
type: "object",
|
|
121
|
+
properties: {
|
|
122
|
+
taskId: { type: "string", description: "Task ID" },
|
|
123
|
+
name: { type: "string", description: "Task name" },
|
|
124
|
+
taskDescription: { type: "string", description: "Task description" },
|
|
125
|
+
priority: { type: "number", description: "Priority" },
|
|
126
|
+
status: { type: "string", description: "Status" },
|
|
127
|
+
subgroupId: { type: "string", description: "Subgroup ID" },
|
|
128
|
+
},
|
|
129
|
+
required: ["taskId"],
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: "complete_task",
|
|
134
|
+
description: "Mark a task as complete",
|
|
135
|
+
inputSchema: {
|
|
136
|
+
type: "object",
|
|
137
|
+
properties: {
|
|
138
|
+
taskId: { type: "string", description: "Task ID" },
|
|
139
|
+
},
|
|
140
|
+
required: ["taskId"],
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: "add_tag_to_task",
|
|
145
|
+
description: "Add a tag to a task",
|
|
146
|
+
inputSchema: {
|
|
147
|
+
type: "object",
|
|
148
|
+
properties: {
|
|
149
|
+
taskId: { type: "string", description: "Task ID" },
|
|
150
|
+
tagId: { type: "string", description: "Tag ID" },
|
|
151
|
+
},
|
|
152
|
+
required: ["taskId", "tagId"],
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: "remove_tag_from_task",
|
|
157
|
+
description: "Remove a tag from a task",
|
|
158
|
+
inputSchema: {
|
|
159
|
+
type: "object",
|
|
160
|
+
properties: {
|
|
161
|
+
taskId: { type: "string", description: "Task ID" },
|
|
162
|
+
tagId: { type: "string", description: "Tag ID" },
|
|
163
|
+
},
|
|
164
|
+
required: ["taskId", "tagId"],
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
170
|
+
return { tools: TOOLS };
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
174
|
+
const { name, arguments: args = {} } = request.params;
|
|
175
|
+
|
|
176
|
+
function success(result) {
|
|
177
|
+
return {
|
|
178
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function error(message) {
|
|
183
|
+
return {
|
|
184
|
+
content: [{ type: "text", text: message }],
|
|
185
|
+
isError: true,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
switch (name) {
|
|
191
|
+
case "list_projects": {
|
|
192
|
+
const result = await apiCall("GET", "/projects");
|
|
193
|
+
return success(result);
|
|
194
|
+
}
|
|
195
|
+
case "get_project_tasks": {
|
|
196
|
+
const { projectId } = args;
|
|
197
|
+
if (!projectId) return error("Missing required argument: projectId");
|
|
198
|
+
const result = await apiCall("GET", `/projects/${encodeURIComponent(projectId)}/tasks`);
|
|
199
|
+
return success(result);
|
|
200
|
+
}
|
|
201
|
+
case "get_project_subgroups": {
|
|
202
|
+
const { projectId } = args;
|
|
203
|
+
if (!projectId) return error("Missing required argument: projectId");
|
|
204
|
+
const result = await apiCall("GET", `/projects/${encodeURIComponent(projectId)}/subgroups`);
|
|
205
|
+
return success(result);
|
|
206
|
+
}
|
|
207
|
+
case "get_project_tags": {
|
|
208
|
+
const { projectId } = args;
|
|
209
|
+
if (!projectId) return error("Missing required argument: projectId");
|
|
210
|
+
const result = await apiCall("GET", `/projects/${encodeURIComponent(projectId)}/tags`);
|
|
211
|
+
return success(result);
|
|
212
|
+
}
|
|
213
|
+
case "get_task": {
|
|
214
|
+
const { taskId } = args;
|
|
215
|
+
if (!taskId) return error("Missing required argument: taskId");
|
|
216
|
+
const result = await apiCall("GET", `/tasks/${encodeURIComponent(taskId)}`);
|
|
217
|
+
return success(result);
|
|
218
|
+
}
|
|
219
|
+
case "create_task": {
|
|
220
|
+
if (!args.projectId || !args.name) return error("Missing required arguments: projectId, name");
|
|
221
|
+
const body = {
|
|
222
|
+
name: args.name,
|
|
223
|
+
...(args.taskDescription != null && { taskDescription: args.taskDescription }),
|
|
224
|
+
...(args.priority != null && { priority: args.priority }),
|
|
225
|
+
...(args.status != null && { status: args.status }),
|
|
226
|
+
...(args.subgroupId != null && { subgroupId: args.subgroupId }),
|
|
227
|
+
};
|
|
228
|
+
const result = await apiCall("POST", `/projects/${encodeURIComponent(args.projectId)}/tasks`, body);
|
|
229
|
+
return success(result);
|
|
230
|
+
}
|
|
231
|
+
case "update_task": {
|
|
232
|
+
const { taskId, ...rest } = args;
|
|
233
|
+
if (!taskId) return error("Missing required argument: taskId");
|
|
234
|
+
const body = {
|
|
235
|
+
...(rest.name != null && { name: rest.name }),
|
|
236
|
+
...(rest.taskDescription != null && { taskDescription: rest.taskDescription }),
|
|
237
|
+
...(rest.priority != null && { priority: rest.priority }),
|
|
238
|
+
...(rest.status != null && { status: rest.status }),
|
|
239
|
+
...(rest.subgroupId != null && { subgroupId: rest.subgroupId }),
|
|
240
|
+
};
|
|
241
|
+
const result = await apiCall("PUT", `/tasks/${encodeURIComponent(taskId)}`, body);
|
|
242
|
+
return success(result);
|
|
243
|
+
}
|
|
244
|
+
case "complete_task": {
|
|
245
|
+
const { taskId } = args;
|
|
246
|
+
if (!taskId) return error("Missing required argument: taskId");
|
|
247
|
+
const result = await apiCall("POST", `/tasks/${encodeURIComponent(taskId)}/complete`);
|
|
248
|
+
return success(result);
|
|
249
|
+
}
|
|
250
|
+
case "add_tag_to_task": {
|
|
251
|
+
const { taskId, tagId } = args;
|
|
252
|
+
if (!taskId || !tagId) return error("Missing required arguments: taskId, tagId");
|
|
253
|
+
const result = await apiCall("POST", `/tasks/${encodeURIComponent(taskId)}/tags/${encodeURIComponent(tagId)}`);
|
|
254
|
+
return success(result);
|
|
255
|
+
}
|
|
256
|
+
case "remove_tag_from_task": {
|
|
257
|
+
const { taskId, tagId } = args;
|
|
258
|
+
if (!taskId || !tagId) return error("Missing required arguments: taskId, tagId");
|
|
259
|
+
const result = await apiCall("DELETE", `/tasks/${encodeURIComponent(taskId)}/tags/${encodeURIComponent(tagId)}`);
|
|
260
|
+
return success(result);
|
|
261
|
+
}
|
|
262
|
+
default:
|
|
263
|
+
return error("Unknown tool");
|
|
264
|
+
}
|
|
265
|
+
} catch (err) {
|
|
266
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
267
|
+
return error(message);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const transport = new StdioServerTransport();
|
|
272
|
+
await server.connect(transport);
|