@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.
Files changed (3) hide show
  1. package/README.md +60 -0
  2. package/package.json +26 -0
  3. 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);