@taskinger/mcp 0.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.
Files changed (3) hide show
  1. package/README.md +71 -0
  2. package/dist/index.js +196 -0
  3. package/package.json +51 -0
package/README.md ADDED
@@ -0,0 +1,71 @@
1
+ # @taskinger/mcp
2
+
3
+ An MCP server for [Taskinger](https://taskinger.app): work in one workbook —
4
+ its tasks and its notes — from Claude Desktop, Claude Code, Cursor or any
5
+ other MCP client.
6
+
7
+ Tasks say what to do. Notes are where an assistant writes down what it
8
+ learned, so the next session does not work it out again.
9
+
10
+ ## Setup
11
+
12
+ Mint a key in the app: **Workbook settings → External API** (Team plan). Give
13
+ it the write scope if the assistant should create and edit things rather than
14
+ only read them.
15
+
16
+ Claude Code:
17
+
18
+ ```bash
19
+ claude mcp add taskinger -e TASKINGER_API_KEY=zad_… -e TASKINGER_WORKBOOK=… -- npx -y @taskinger/mcp
20
+ ```
21
+
22
+ Claude Desktop (`claude_desktop_config.json`) or Cursor (`mcp.json`):
23
+
24
+ ```json
25
+ {
26
+ "mcpServers": {
27
+ "taskinger": {
28
+ "command": "npx",
29
+ "args": ["-y", "@taskinger/mcp"],
30
+ "env": {
31
+ "TASKINGER_API_KEY": "zad_…",
32
+ "TASKINGER_WORKBOOK": "the workbook id, as in https://taskinger.app/w/<id>"
33
+ }
34
+ }
35
+ }
36
+ }
37
+ ```
38
+
39
+ | Variable | |
40
+ |---|---|
41
+ | `TASKINGER_API_KEY` | `zad_<keyId>.<secret>`, from Workbook settings → External API |
42
+ | `TASKINGER_WORKBOOK` | the workbook id from its address |
43
+ | `TASKINGER_API_BASE` | optional; defaults to `https://taskinger.app/api` |
44
+
45
+ ## Tools
46
+
47
+ **Tasks** — `list_tasks`, `get_task`, `create_task`, `update_task`,
48
+ `complete_task`.
49
+
50
+ **Notes** — `list_notes`, `get_note`, `create_note`, `update_note`. A note is
51
+ written prose; a list is ticked off. A list's lines are edited by name (add,
52
+ tick by line id, remove by line id), so an edit here never overwrites a line
53
+ somebody changed in the app meanwhile.
54
+
55
+ **Lookups** — `get_workbook` (including the house rules on task intake),
56
+ `list_members`, `list_projects`, `list_labels`.
57
+
58
+ ## Scope
59
+
60
+ One workbook per server process, named by the environment, so an assistant
61
+ only ever sees and touches what the key was minted for — a key cannot reach
62
+ another workbook. A read-only key gives a read-only assistant. Everything the
63
+ server writes goes through the same API the app's own integrations use, so it
64
+ lands in the activity log, pushes and webhooks like any other change, and the
65
+ workbook's house rules are enforced.
66
+
67
+ Full API reference: [taskinger.app/openapi.yaml](https://taskinger.app/openapi.yaml).
68
+
69
+ ## License
70
+
71
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,196 @@
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 { z } from 'zod';
5
+ /**
6
+ * Taskinger MCP server: a thin, stdio-speaking wrapper over the task API
7
+ * (https://taskinger.app/openapi.yaml). One workbook per process, named by
8
+ * the environment, so an assistant can only ever see and touch what the key
9
+ * was minted for.
10
+ *
11
+ * TASKINGER_API_KEY zad_<keyId>.<secret> (Workbook settings → External API)
12
+ * TASKINGER_WORKBOOK the workbook id, as in https://taskinger.app/w/<id>
13
+ * TASKINGER_API_BASE optional; defaults to https://taskinger.app/api
14
+ */
15
+ const KEY = process.env.TASKINGER_API_KEY;
16
+ const WS = process.env.TASKINGER_WORKBOOK;
17
+ const BASE = (process.env.TASKINGER_API_BASE ?? 'https://taskinger.app/api').replace(/\/+$/, '');
18
+ if (!KEY || !WS) {
19
+ console.error('Set TASKINGER_API_KEY and TASKINGER_WORKBOOK. Both come from Workbook settings → External API.');
20
+ process.exit(1);
21
+ }
22
+ async function call(method, path, body, query) {
23
+ const url = new URL(`${BASE}/v1/workbooks/${WS}${path}`);
24
+ for (const [k, v] of Object.entries(query ?? {}))
25
+ if (v !== undefined && v !== '')
26
+ url.searchParams.set(k, v);
27
+ const res = await fetch(url, {
28
+ method,
29
+ headers: {
30
+ Authorization: `Bearer ${KEY}`,
31
+ 'Content-Type': 'application/json',
32
+ 'User-Agent': 'taskinger-mcp/0.1',
33
+ },
34
+ body: body === undefined ? undefined : JSON.stringify(body),
35
+ });
36
+ const text = await res.text();
37
+ let data;
38
+ try {
39
+ data = JSON.parse(text);
40
+ }
41
+ catch {
42
+ data = { error: text };
43
+ }
44
+ if (!res.ok) {
45
+ const message = data?.error ?? `HTTP ${res.status}`;
46
+ throw new Error(`${message} (HTTP ${res.status})`);
47
+ }
48
+ return data;
49
+ }
50
+ const json = (value) => ({ content: [{ type: 'text', text: JSON.stringify(value, null, 2) }] });
51
+ const failure = (error) => ({
52
+ content: [{ type: 'text', text: error instanceof Error ? error.message : String(error) }],
53
+ isError: true,
54
+ });
55
+ const server = new McpServer({ name: 'taskinger', version: '0.1.0' });
56
+ const statusEnum = z.enum(['todo', 'in_progress', 'awaiting_approval', 'done']);
57
+ const taskFields = {
58
+ title: z.string().max(200).optional().describe('The task title'),
59
+ description: z.string().max(20000).optional().describe('Markdown'),
60
+ priority: z.number().int().min(1).max(4).optional().describe('1 urgent, 2 high, 3 normal, 4 someday'),
61
+ assigneeUids: z.array(z.string()).optional().describe('Member uids from list_members'),
62
+ labelIds: z.array(z.string()).max(20).optional().describe('Label ids from list_labels'),
63
+ projectId: z.string().nullable().optional().describe('A project id from list_projects, or null for the workbook root'),
64
+ dueDate: z.string().nullable().optional().describe('ISO-8601 date-time, or null to clear'),
65
+ dueHasTime: z.boolean().optional().describe('Whether dueDate carries a clock time; false means all day'),
66
+ checklist: z.array(z.string()).max(50).optional().describe('Checklist steps, one string each'),
67
+ };
68
+ server.tool('get_workbook', 'The workbook this server is connected to: its name, type and house rules (required due date, assignee, completion note, sign-off).', {}, async () => {
69
+ try {
70
+ return json(await call('GET', ''));
71
+ }
72
+ catch (error) {
73
+ return failure(error);
74
+ }
75
+ });
76
+ server.tool('list_tasks', 'List tasks, most recently updated first. Filter by status (comma-separated: todo, in_progress, awaiting_approval, done), assignee uid, project id (or "root"), or updatedSince (ISO-8601). Up to 200.', {
77
+ status: z.string().optional(),
78
+ assignee: z.string().optional(),
79
+ projectId: z.string().optional(),
80
+ updatedSince: z.string().optional(),
81
+ limit: z.number().int().min(1).max(200).optional(),
82
+ }, async (args) => {
83
+ try {
84
+ return json(await call('GET', '/tasks', undefined, { ...args, limit: args.limit?.toString() }));
85
+ }
86
+ catch (error) {
87
+ return failure(error);
88
+ }
89
+ });
90
+ server.tool('get_task', 'One task by id, with its checklist, dates, assignees and address in the app.', { taskId: z.string() }, async ({ taskId }) => {
91
+ try {
92
+ return json(await call('GET', `/tasks/${encodeURIComponent(taskId)}`));
93
+ }
94
+ catch (error) {
95
+ return failure(error);
96
+ }
97
+ });
98
+ server.tool('create_task', 'Create a task. Only the title is required; the workbook may also require a due date or an assignee (see get_workbook).', { ...taskFields, title: z.string().min(1).max(200) }, async (args) => {
99
+ try {
100
+ return json(await call('POST', '/tasks', args));
101
+ }
102
+ catch (error) {
103
+ return failure(error);
104
+ }
105
+ });
106
+ server.tool('update_task', 'Change fields of a task. Only the fields given change. Set status to move it; finishing (done or awaiting_approval) may require completionNote.', { taskId: z.string(), ...taskFields, status: statusEnum.optional(), completionNote: z.string().max(2000).nullable().optional() }, async ({ taskId, ...patch }) => {
107
+ try {
108
+ return json(await call('PATCH', `/tasks/${encodeURIComponent(taskId)}`, patch));
109
+ }
110
+ catch (error) {
111
+ return failure(error);
112
+ }
113
+ });
114
+ server.tool('complete_task', 'Mark a task done, with an optional note saying what was done (required when the workbook asks for one).', { taskId: z.string(), note: z.string().max(2000).optional() }, async ({ taskId, note }) => {
115
+ try {
116
+ return json(await call('POST', `/tasks/${encodeURIComponent(taskId)}/complete`, note === undefined ? {} : { note }));
117
+ }
118
+ catch (error) {
119
+ return failure(error);
120
+ }
121
+ });
122
+ /* Notes are where an assistant writes down what it learned, so the next
123
+ * session does not derive it again: prose (`note`) or a ticked `list`. */
124
+ server.tool('list_notes', 'List notes and lists, most recently updated first. Filter by kind ("note" or "list"), project id (or "root" for the workbook\'s general shelf), or updatedSince (ISO-8601). Up to 200.', {
125
+ kind: z.enum(['note', 'list']).optional(),
126
+ projectId: z.string().optional(),
127
+ updatedSince: z.string().optional(),
128
+ limit: z.number().int().min(1).max(200).optional(),
129
+ }, async (args) => {
130
+ try {
131
+ return json(await call('GET', '/notes', undefined, { ...args, limit: args.limit?.toString() }));
132
+ }
133
+ catch (error) {
134
+ return failure(error);
135
+ }
136
+ });
137
+ server.tool('get_note', 'One note by id, with its body or its lines and their ids.', { noteId: z.string() }, async ({ noteId }) => {
138
+ try {
139
+ return json(await call('GET', `/notes/${encodeURIComponent(noteId)}`));
140
+ }
141
+ catch (error) {
142
+ return failure(error);
143
+ }
144
+ });
145
+ server.tool('create_note', 'Write a note into the workbook, or into one of its projects — the place to record a decision, a gotcha or a piece of context worth keeping. Use kind "list" with items for something to tick off instead.', {
146
+ title: z.string().min(1).max(200),
147
+ body: z.string().max(20000).optional().describe('Markdown. The note itself.'),
148
+ kind: z.enum(['note', 'list']).optional().describe('Default "note". Fixed at creation.'),
149
+ items: z.array(z.string().max(120)).max(200).optional().describe('Lines, only on a list'),
150
+ projectId: z.string().nullable().optional().describe('A project id from list_projects, or null for the workbook\'s general shelf'),
151
+ pinned: z.boolean().optional(),
152
+ }, async (args) => {
153
+ try {
154
+ return json(await call('POST', '/notes', args));
155
+ }
156
+ catch (error) {
157
+ return failure(error);
158
+ }
159
+ });
160
+ server.tool('update_note', 'Change a note: its title, body or pinning. On a list, edit lines by name — add new ones, tick them by id, remove them by id — which never overwrites a line somebody else changed meanwhile. Line ids come from get_note.', {
161
+ noteId: z.string(),
162
+ title: z.string().min(1).max(200).optional(),
163
+ body: z.string().max(20000).optional(),
164
+ pinned: z.boolean().optional(),
165
+ items: z
166
+ .object({
167
+ add: z.array(z.string().max(120)).optional(),
168
+ done: z.record(z.boolean()).optional().describe('Line id → ticked or not'),
169
+ remove: z.array(z.string()).optional().describe('Line ids'),
170
+ })
171
+ .optional()
172
+ .describe('Lists only'),
173
+ }, async ({ noteId, ...patch }) => {
174
+ try {
175
+ return json(await call('PATCH', `/notes/${encodeURIComponent(noteId)}`, patch));
176
+ }
177
+ catch (error) {
178
+ return failure(error);
179
+ }
180
+ });
181
+ for (const [name, path, what] of [
182
+ ['list_members', '/members', 'Members of the workbook, with the uids used for assigning.'],
183
+ ['list_projects', '/projects', 'Projects of the workbook, with the ids used for filing.'],
184
+ ['list_labels', '/labels', 'Labels of the workbook, with the ids used on tasks.'],
185
+ ]) {
186
+ server.tool(name, what, {}, async () => {
187
+ try {
188
+ return json(await call('GET', path));
189
+ }
190
+ catch (error) {
191
+ return failure(error);
192
+ }
193
+ });
194
+ }
195
+ const transport = new StdioServerTransport();
196
+ await server.connect(transport);
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@taskinger/mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for Taskinger: read and write the tasks and notes of one workbook from Claude, Cursor or any MCP client.",
5
+ "license": "MIT",
6
+ "author": "Taskinger",
7
+ "homepage": "https://taskinger.app",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/MegaVmetal/Taskinger.app.git",
11
+ "directory": "mcp"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/MegaVmetal/Taskinger.app/issues"
15
+ },
16
+ "keywords": [
17
+ "mcp",
18
+ "model-context-protocol",
19
+ "taskinger",
20
+ "tasks",
21
+ "notes",
22
+ "claude",
23
+ "cursor"
24
+ ],
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "type": "module",
29
+ "bin": {
30
+ "taskinger-mcp": "dist/index.js"
31
+ },
32
+ "files": [
33
+ "dist"
34
+ ],
35
+ "scripts": {
36
+ "build": "tsc",
37
+ "start": "node dist/index.js",
38
+ "typecheck": "tsc --noEmit"
39
+ },
40
+ "engines": {
41
+ "node": ">=18"
42
+ },
43
+ "dependencies": {
44
+ "@modelcontextprotocol/sdk": "^1.12.0",
45
+ "zod": "^3.24.0"
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "^22.0.0",
49
+ "typescript": "^5.6.0"
50
+ }
51
+ }