tidee-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/LICENSE +21 -0
- package/README.md +105 -0
- package/dist/index.js +434 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 tidee Ltd
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# tidee-life-mcp
|
|
2
|
+
|
|
3
|
+
An [MCP](https://modelcontextprotocol.io) server for tidee. It lets an
|
|
4
|
+
assistant add items to your workspace, read what is there, and mark
|
|
5
|
+
things done — without you switching to the app to write it down.
|
|
6
|
+
|
|
7
|
+
## Which one do you want?
|
|
8
|
+
|
|
9
|
+
There are two ways to connect, and most people want the hosted one.
|
|
10
|
+
|
|
11
|
+
**Hosted** — point your client at `https://app.tidee.com/mcp`. Nothing to
|
|
12
|
+
install, and no token to copy: your client registers itself, tidee asks
|
|
13
|
+
you to approve it in the browser, and that is the whole setup. In Claude
|
|
14
|
+
Code that is one line:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
claude mcp add --transport http tidee https://app.tidee.com/mcp
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**This package** — a local server over stdio, for clients that do not
|
|
21
|
+
speak HTTP MCP, or when you want to point at a development server. It
|
|
22
|
+
needs Node and a token you paste into a config file.
|
|
23
|
+
|
|
24
|
+
## Setup
|
|
25
|
+
|
|
26
|
+
1. In tidee, open **Application Settings → Integrations** and create a
|
|
27
|
+
token. Give it only the access the assistant needs:
|
|
28
|
+
|
|
29
|
+
| Access | Tools it enables |
|
|
30
|
+
|---|---|
|
|
31
|
+
| Add new items | `capture`, `create_item` |
|
|
32
|
+
| Read items | `list_boxes`, `list_items` |
|
|
33
|
+
| Edit and complete existing items | `complete_item` |
|
|
34
|
+
|
|
35
|
+
The token is shown once. Copy it then.
|
|
36
|
+
|
|
37
|
+
2. Add the server to your MCP client's config:
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"mcpServers": {
|
|
42
|
+
"tidee": {
|
|
43
|
+
"command": "npx",
|
|
44
|
+
"args": ["-y", "tidee-mcp"],
|
|
45
|
+
"env": { "TIDEE_API_TOKEN": "tdl_…" }
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
3. Restart the client.
|
|
52
|
+
|
|
53
|
+
## Environment
|
|
54
|
+
|
|
55
|
+
| Variable | Required | Default |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| `TIDEE_API_TOKEN` | yes | — |
|
|
58
|
+
| `TIDEE_API_URL` | no | `https://app.tidee.com` |
|
|
59
|
+
|
|
60
|
+
`TIDEE_API_URL` exists for pointing at a local server during
|
|
61
|
+
development. Leave it unset otherwise.
|
|
62
|
+
|
|
63
|
+
## Building
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
pnpm run build
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Type-checks, then bundles to a single `dist/index.js` with esbuild.
|
|
70
|
+
|
|
71
|
+
The bundling is not an optimisation. The tool definitions — names,
|
|
72
|
+
schemas, descriptions, and the sentences returned to the model — live in
|
|
73
|
+
`tidee-life-core/mcp`, shared with the hosted server so the two cannot
|
|
74
|
+
drift. That is a private workspace package, so it cannot be a dependency
|
|
75
|
+
of anything published; inlining it means what ships depends only on
|
|
76
|
+
`@modelcontextprotocol/sdk` and `zod`, while there is still exactly one
|
|
77
|
+
definition of the tools.
|
|
78
|
+
|
|
79
|
+
## Tools
|
|
80
|
+
|
|
81
|
+
- **`capture`** — the one to reach for. Takes a whole thought in ordinary
|
|
82
|
+
language and lets tidee work out the separate items, which box each
|
|
83
|
+
belongs in, and any dates. "Book the MOT and call Sam about Friday"
|
|
84
|
+
becomes two items, filed.
|
|
85
|
+
- **`create_item`** — one item with an exact title, for when the wording
|
|
86
|
+
is already decided.
|
|
87
|
+
- **`list_boxes`** — the boxes items are filed in, with their ids.
|
|
88
|
+
- **`list_items`** — read or search items.
|
|
89
|
+
- **`complete_item`** — mark one done.
|
|
90
|
+
|
|
91
|
+
## Notes
|
|
92
|
+
|
|
93
|
+
Every tool is a single call to tidee's public API. Planning, filing and
|
|
94
|
+
the never-lose fallback all happen server-side, which is why this server
|
|
95
|
+
holds no logic worth configuring: it is a client, not a second copy of
|
|
96
|
+
the app.
|
|
97
|
+
|
|
98
|
+
A capture that cannot be interpreted is never discarded — it is stored
|
|
99
|
+
verbatim as one item, and the tool says so.
|
|
100
|
+
|
|
101
|
+
Captures made through this server count towards the account's capture
|
|
102
|
+
allowance, exactly as they would in the app.
|
|
103
|
+
|
|
104
|
+
If a token is revoked or expires the tools stop working immediately and
|
|
105
|
+
say why. Create a new one in Settings and update the config.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
|
|
7
|
+
// ../tidee-life-core/dist/src/mcp/tools.js
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
|
|
10
|
+
// ../tidee-life-config/dist/src/item-types.js
|
|
11
|
+
var ITEM_TYPES = {
|
|
12
|
+
search: {
|
|
13
|
+
icon: null,
|
|
14
|
+
name: "search",
|
|
15
|
+
label: "Search",
|
|
16
|
+
isPrimary: false
|
|
17
|
+
},
|
|
18
|
+
todoBox: {
|
|
19
|
+
icon: "tidee-box",
|
|
20
|
+
name: "todoBox",
|
|
21
|
+
label: "Box",
|
|
22
|
+
isPrimary: true
|
|
23
|
+
},
|
|
24
|
+
todoText: {
|
|
25
|
+
icon: null,
|
|
26
|
+
name: "todoText",
|
|
27
|
+
label: "Text",
|
|
28
|
+
isPrimary: true
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// ../tidee-life-config/dist/src/api-token.js
|
|
33
|
+
var API_TOKEN_SCOPES = {
|
|
34
|
+
/** Create items from free text through the capture service. */
|
|
35
|
+
capture: "capture",
|
|
36
|
+
/** Read items, boxes and the workspace. */
|
|
37
|
+
itemsRead: "items:read",
|
|
38
|
+
/** Create, edit, complete and move items. */
|
|
39
|
+
itemsWrite: "items:write"
|
|
40
|
+
};
|
|
41
|
+
var ALL_API_TOKEN_SCOPES = Object.values(API_TOKEN_SCOPES);
|
|
42
|
+
var KNOWN_SCOPES = new Set(ALL_API_TOKEN_SCOPES);
|
|
43
|
+
var API_TOKEN_EXPIRY_OPTION_DAYS = [90, 365, null];
|
|
44
|
+
var KNOWN_EXPIRY_DAYS = new Set(API_TOKEN_EXPIRY_OPTION_DAYS);
|
|
45
|
+
|
|
46
|
+
// ../tidee-life-config/dist/src/capture-source.js
|
|
47
|
+
var CAPTURE_SOURCE = {
|
|
48
|
+
/** Quick Capture manual mode — user typed a title. */
|
|
49
|
+
manual: "manual",
|
|
50
|
+
/** Quick Capture AI mode — dictated, planned, and reviewed before commit. */
|
|
51
|
+
ai: "ai",
|
|
52
|
+
/** Inline add on a pane row or mobile screen. */
|
|
53
|
+
inline: "inline",
|
|
54
|
+
/**
|
|
55
|
+
* Dictated and committed WITHOUT the user seeing the plan. The only
|
|
56
|
+
* source that starts life unreviewed, and the only one that stores a
|
|
57
|
+
* transcript. Surfaced afterwards in the capture review strip.
|
|
58
|
+
*/
|
|
59
|
+
eyesFree: "eyesFree",
|
|
60
|
+
/**
|
|
61
|
+
* Phase 4 — arrived from Slack (`/tidee`, an `@tidee` mention, or the
|
|
62
|
+
* message shortcut). Counted: Slack is a Free-tier feature precisely
|
|
63
|
+
* because its captures count. Free plus uncounted would be an
|
|
64
|
+
* unlimited capture path around the 50.
|
|
65
|
+
*/
|
|
66
|
+
slack: "slack",
|
|
67
|
+
/**
|
|
68
|
+
* Phase 4 — arrived through the MCP server or the public API. Counted:
|
|
69
|
+
* an MCP `capture` runs the same planner at the same cost as an in-app
|
|
70
|
+
* AI capture, and a literal `create_item` is still an item the user now
|
|
71
|
+
* has. Exempting either would make the cap bypassable by anyone willing
|
|
72
|
+
* to install a config file.
|
|
73
|
+
*/
|
|
74
|
+
mcp: "mcp"
|
|
75
|
+
};
|
|
76
|
+
var COUNTED_CAPTURE_SOURCES = [
|
|
77
|
+
CAPTURE_SOURCE.manual,
|
|
78
|
+
CAPTURE_SOURCE.ai,
|
|
79
|
+
CAPTURE_SOURCE.inline,
|
|
80
|
+
CAPTURE_SOURCE.eyesFree,
|
|
81
|
+
CAPTURE_SOURCE.slack,
|
|
82
|
+
CAPTURE_SOURCE.mcp
|
|
83
|
+
];
|
|
84
|
+
var KNOWN_CAPTURE_SOURCES = new Set(Object.values(CAPTURE_SOURCE));
|
|
85
|
+
|
|
86
|
+
// ../tidee-life-config/dist/src/item-ownership.js
|
|
87
|
+
var ITEM_OWNERSHIP = {
|
|
88
|
+
owner: {
|
|
89
|
+
id: "owner",
|
|
90
|
+
name: "item.ownership.owner.text",
|
|
91
|
+
capabilities: [6, 1, 2, 3, 4, 5, 7, 8]
|
|
92
|
+
},
|
|
93
|
+
editor: {
|
|
94
|
+
id: "editor",
|
|
95
|
+
name: "item.ownership.editor.text",
|
|
96
|
+
capabilities: [6, 1, 2]
|
|
97
|
+
},
|
|
98
|
+
readonly: {
|
|
99
|
+
id: "readonly",
|
|
100
|
+
name: "item.ownership.readonly.text",
|
|
101
|
+
capabilities: [6]
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
var ITEM_OWNERSHIP_CAPABILITIES = {
|
|
105
|
+
1: {
|
|
106
|
+
category: 1,
|
|
107
|
+
desc: "item.ownership.capability.1"
|
|
108
|
+
},
|
|
109
|
+
2: {
|
|
110
|
+
category: 2,
|
|
111
|
+
desc: "item.ownership.capability.2"
|
|
112
|
+
},
|
|
113
|
+
3: {
|
|
114
|
+
category: 1,
|
|
115
|
+
desc: "item.ownership.capability.3"
|
|
116
|
+
},
|
|
117
|
+
4: {
|
|
118
|
+
category: 1,
|
|
119
|
+
desc: "item.ownership.capability.4"
|
|
120
|
+
},
|
|
121
|
+
5: {
|
|
122
|
+
category: 1,
|
|
123
|
+
desc: "item.ownership.capability.5"
|
|
124
|
+
},
|
|
125
|
+
6: {
|
|
126
|
+
category: 1,
|
|
127
|
+
desc: "item.ownership.capability.6"
|
|
128
|
+
},
|
|
129
|
+
7: {
|
|
130
|
+
category: 1,
|
|
131
|
+
desc: "item.ownership.capability.7"
|
|
132
|
+
},
|
|
133
|
+
8: {
|
|
134
|
+
category: 1,
|
|
135
|
+
desc: "item.ownership.capability.8"
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
var ITEM_OWNERSHIP_OPTIONS = Object.keys(ITEM_OWNERSHIP).map((ownershipId) => ({
|
|
139
|
+
label: ITEM_OWNERSHIP[ownershipId].name,
|
|
140
|
+
value: ITEM_OWNERSHIP[ownershipId].id
|
|
141
|
+
}));
|
|
142
|
+
var ITEM_OWNERSHIP_CAPABILITY_LOOKUP = Object.keys(ITEM_OWNERSHIP).reduce((all, current) => {
|
|
143
|
+
const capabilityDescs = ITEM_OWNERSHIP[current].capabilities.map((capabilityId) => ITEM_OWNERSHIP_CAPABILITIES[capabilityId].desc);
|
|
144
|
+
return {
|
|
145
|
+
...all,
|
|
146
|
+
[current]: capabilityDescs
|
|
147
|
+
};
|
|
148
|
+
}, {});
|
|
149
|
+
var DEFAULT_ITEM_OWNERSHIP = ITEM_OWNERSHIP.readonly.id;
|
|
150
|
+
|
|
151
|
+
// ../tidee-life-config/dist/src/organisation-ownership.js
|
|
152
|
+
var ORGANISATION_OWNERSHIP_CAPABILITIES = {
|
|
153
|
+
1: {
|
|
154
|
+
category: 1,
|
|
155
|
+
desc: "organisations.modal.organisation.ownership.capability.1"
|
|
156
|
+
},
|
|
157
|
+
2: {
|
|
158
|
+
category: 2,
|
|
159
|
+
desc: "organisations.modal.organisation.ownership.capability.2"
|
|
160
|
+
},
|
|
161
|
+
4: {
|
|
162
|
+
category: 4,
|
|
163
|
+
desc: "organisations.modal.organisation.ownership.capability.4"
|
|
164
|
+
},
|
|
165
|
+
5: {
|
|
166
|
+
category: 5,
|
|
167
|
+
desc: "organisations.modal.organisation.ownership.capability.5"
|
|
168
|
+
},
|
|
169
|
+
// Phase 3 slice N — admins can transfer ownership of items
|
|
170
|
+
// in the org, even ones they don't own themselves (the
|
|
171
|
+
// offboarding path). Enforced server-side by transferItem
|
|
172
|
+
// Ownership; owner-only sharing still holds.
|
|
173
|
+
6: {
|
|
174
|
+
category: 6,
|
|
175
|
+
desc: "organisations.modal.organisation.ownership.capability.6"
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
var ORGANISATION_OWNERSHIP_TYPES = {
|
|
179
|
+
admin: {
|
|
180
|
+
id: "admin",
|
|
181
|
+
name: "organisation.ownership.administrator.text",
|
|
182
|
+
capabilities: [1, 2, 4, 5, 6]
|
|
183
|
+
},
|
|
184
|
+
member: {
|
|
185
|
+
id: "member",
|
|
186
|
+
name: "organisation.ownership.member.text",
|
|
187
|
+
capabilities: []
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
var ORGANISATION_OWNERSHIP_OPTIONS = Object.keys(ORGANISATION_OWNERSHIP_TYPES).map((ownershipId) => ({
|
|
191
|
+
label: ORGANISATION_OWNERSHIP_TYPES[ownershipId].name,
|
|
192
|
+
value: ORGANISATION_OWNERSHIP_TYPES[ownershipId].id
|
|
193
|
+
}));
|
|
194
|
+
var ORGANISATION_OWNERSHIP_CAPABILITY_LOOKUP = Object.keys(ORGANISATION_OWNERSHIP_TYPES).reduce((all, current) => {
|
|
195
|
+
const capabilityDescs = ORGANISATION_OWNERSHIP_TYPES[current].capabilities.map((capabilityId) => ORGANISATION_OWNERSHIP_CAPABILITIES[capabilityId].desc);
|
|
196
|
+
return {
|
|
197
|
+
...all,
|
|
198
|
+
[current]: capabilityDescs
|
|
199
|
+
};
|
|
200
|
+
}, { admin: [], member: [] });
|
|
201
|
+
|
|
202
|
+
// ../tidee-life-config/dist/src/oauth.js
|
|
203
|
+
var OAUTH_ACCESS_TOKEN_TTL_SECONDS = 60 * 60;
|
|
204
|
+
var OAUTH_REFRESH_TOKEN_TTL_SECONDS = 60 * 60 * 24 * 60;
|
|
205
|
+
var OAUTH_REGISTRATION_WINDOW_MS = 60 * 60 * 1e3;
|
|
206
|
+
|
|
207
|
+
// ../tidee-life-config/dist/src/index.js
|
|
208
|
+
var ALLOWED_PRIORITIZABLE_TYPES = [ITEM_TYPES.todoText.name];
|
|
209
|
+
|
|
210
|
+
// ../tidee-life-core/dist/src/mcp/tools.js
|
|
211
|
+
var describeToolError = (failure) => {
|
|
212
|
+
const { code, status, message } = failure ?? {};
|
|
213
|
+
if (code === "error.free.tier.capture.limit") {
|
|
214
|
+
return "This tidee account has used all its free captures. The item was not saved. Upgrading in tidee removes the limit.";
|
|
215
|
+
}
|
|
216
|
+
if (code === "error.api.token.scope.missing") {
|
|
217
|
+
return "This tidee token is not permitted to do that. Create a token with the needed access in Application Settings \u2192 Integrations.";
|
|
218
|
+
}
|
|
219
|
+
if (status === 401) {
|
|
220
|
+
return "This tidee token is not valid \u2014 it may have been revoked or expired. Create a new one in Application Settings \u2192 Integrations.";
|
|
221
|
+
}
|
|
222
|
+
if (code)
|
|
223
|
+
return code;
|
|
224
|
+
if (status)
|
|
225
|
+
return `tidee returned ${status}.`;
|
|
226
|
+
return message || "The request to tidee failed.";
|
|
227
|
+
};
|
|
228
|
+
var tool = (spec) => spec;
|
|
229
|
+
var TIDEE_TOOLS = [
|
|
230
|
+
tool({
|
|
231
|
+
name: "capture",
|
|
232
|
+
title: "Capture into tidee",
|
|
233
|
+
scope: API_TOKEN_SCOPES.capture,
|
|
234
|
+
// Written to be CHOSEN correctly rather than to document an
|
|
235
|
+
// endpoint. The failure mode this description exists to prevent
|
|
236
|
+
// is an assistant looping create_item once per sentence.
|
|
237
|
+
description: 'Add one or more items to the user\u2019s tidee workspace from ordinary language. Give it the whole thought as the user said it \u2014 "book the MOT and call Sam about Friday" \u2014 and tidee works out the separate items, which box each belongs in, and any dates. It REWORDS as it files: "get some milk tomorrow" becomes an item called "milk" with a date. That is wanted when the user is thinking aloud, and wrong when they have chosen their words \u2014 if they quoted a title, use create_item instead. Nothing is ever lost: if the text cannot be interpreted it is stored verbatim as a single item.',
|
|
238
|
+
inputSchema: {
|
|
239
|
+
text: z.string().describe("What the user wants to remember, in their own words."),
|
|
240
|
+
parentId: z.string().optional().describe("Box id to put everything in. Omit to let tidee choose, which is usually right. Ids come from list_boxes.")
|
|
241
|
+
},
|
|
242
|
+
async run(api2, { text, parentId }) {
|
|
243
|
+
const data = await api2.capture({ text, parentId });
|
|
244
|
+
const titles = (data.created ?? []).map((c) => c.title);
|
|
245
|
+
if (titles.length === 0)
|
|
246
|
+
return "Nothing was created.";
|
|
247
|
+
const quoted = titles.map((t) => `"${t}"`).join(", ");
|
|
248
|
+
if (data.usedFallback) {
|
|
249
|
+
return `Saved as one item: ${quoted}. tidee could not split that into separate items, so it kept the text as given.`;
|
|
250
|
+
}
|
|
251
|
+
return `Added ${titles.length} item(s) to tidee: ${quoted}`;
|
|
252
|
+
}
|
|
253
|
+
}),
|
|
254
|
+
tool({
|
|
255
|
+
name: "create_item",
|
|
256
|
+
title: "Create one tidee item",
|
|
257
|
+
scope: API_TOKEN_SCOPES.itemsWrite,
|
|
258
|
+
description: 'Add a single item with an exact title, optionally with a date. The title is stored EXACTLY as given \u2014 nothing rewrites or shortens it. Use this whenever the user has settled on wording: they put it in quotes, they dictated it, or you are copying something verbatim. Quotation marks in particular mean "these words" \u2014 use this tool and pass what is inside them, and put any date in targetDate rather than leaving it in the title. Use capture instead only when the user is talking loosely and wants tidee to work out the wording, the boxes and the dates for them.',
|
|
259
|
+
inputSchema: {
|
|
260
|
+
title: z.string().describe("The item title, used exactly as given."),
|
|
261
|
+
parentId: z.string().optional().describe("Box id to file it in. Ids come from list_boxes."),
|
|
262
|
+
targetDate: z.string().optional().describe('When the user should DO this, as local time with no timezone: "2026-09-10T09:00:00". Not the date of an event the task is merely about \u2014 "book a table for the 14th" needs doing before the 14th, so leave it out and let the user choose. Default to 09:00 when no time was given. Omit when no date was mentioned.')
|
|
263
|
+
},
|
|
264
|
+
async run(api2, { title, parentId, targetDate }) {
|
|
265
|
+
await api2.createItem({ title, parentId, targetDate });
|
|
266
|
+
if (targetDate) {
|
|
267
|
+
const day = String(targetDate).slice(0, 10);
|
|
268
|
+
return `Added "${title}" to tidee, for ${day}.`;
|
|
269
|
+
}
|
|
270
|
+
return `Added "${title}" to tidee.`;
|
|
271
|
+
}
|
|
272
|
+
}),
|
|
273
|
+
tool({
|
|
274
|
+
name: "list_boxes",
|
|
275
|
+
title: "List tidee boxes",
|
|
276
|
+
scope: API_TOKEN_SCOPES.itemsRead,
|
|
277
|
+
description: "The user\u2019s boxes \u2014 the containers items are filed in. Call this when you need a box id for capture or create_item, or when the user asks what they have.",
|
|
278
|
+
inputSchema: {},
|
|
279
|
+
async run(api2) {
|
|
280
|
+
const data = await api2.listBoxes();
|
|
281
|
+
const boxes = data.boxes ?? [];
|
|
282
|
+
if (boxes.length === 0)
|
|
283
|
+
return "This workspace has no boxes yet.";
|
|
284
|
+
return boxes.map((b) => `${b.name} (id: ${b.id})`).join("\n");
|
|
285
|
+
}
|
|
286
|
+
}),
|
|
287
|
+
tool({
|
|
288
|
+
name: "list_items",
|
|
289
|
+
title: "List or search tidee items",
|
|
290
|
+
scope: API_TOKEN_SCOPES.itemsRead,
|
|
291
|
+
description: "Read items from the workspace. Narrow with q to search titles, or parentId to look inside one box. Use this before completing something, to find the item\u2019s id.",
|
|
292
|
+
inputSchema: {
|
|
293
|
+
q: z.string().optional().describe("Search text matched against titles."),
|
|
294
|
+
parentId: z.string().optional().describe("Only items inside this box."),
|
|
295
|
+
limit: z.number().optional().describe("How many to return. Defaults to 50, capped at 200.")
|
|
296
|
+
},
|
|
297
|
+
async run(api2, { q, parentId, limit }) {
|
|
298
|
+
const data = await api2.listItems({ q, parentId, limit });
|
|
299
|
+
const items = data.items ?? [];
|
|
300
|
+
if (items.length === 0)
|
|
301
|
+
return "No matching items.";
|
|
302
|
+
return items.map((i) => {
|
|
303
|
+
const due = i.targetDate ? ` \u2014 due ${String(i.targetDate).slice(0, 10)}` : "";
|
|
304
|
+
return `${i.completed ? "[done] " : ""}${i.title} (id: ${i.id})${due}`;
|
|
305
|
+
}).join("\n");
|
|
306
|
+
}
|
|
307
|
+
}),
|
|
308
|
+
tool({
|
|
309
|
+
name: "complete_item",
|
|
310
|
+
title: "Complete a tidee item",
|
|
311
|
+
scope: API_TOKEN_SCOPES.itemsWrite,
|
|
312
|
+
description: "Mark an item done. Needs the item\u2019s id, which comes from list_items \u2014 find the item first rather than guessing an id.",
|
|
313
|
+
inputSchema: {
|
|
314
|
+
id: z.string().describe("The item id, as returned by list_items.")
|
|
315
|
+
},
|
|
316
|
+
async run(api2, { id }) {
|
|
317
|
+
await api2.completeItem({ id });
|
|
318
|
+
return "Marked complete in tidee.";
|
|
319
|
+
}
|
|
320
|
+
})
|
|
321
|
+
];
|
|
322
|
+
|
|
323
|
+
// src/index.ts
|
|
324
|
+
var API_URL = (process.env.TIDEE_API_URL || "https://app.tidee.com").replace(
|
|
325
|
+
/\/+$/,
|
|
326
|
+
""
|
|
327
|
+
);
|
|
328
|
+
var API_TOKEN = process.env.TIDEE_API_TOKEN;
|
|
329
|
+
if (!API_TOKEN) {
|
|
330
|
+
process.stderr.write(
|
|
331
|
+
"tidee-mcp: TIDEE_API_TOKEN is not set. Create a token in tidee under Application Settings \u2192 Integrations, then set it in this server\u2019s environment.\n"
|
|
332
|
+
);
|
|
333
|
+
process.exit(1);
|
|
334
|
+
}
|
|
335
|
+
var HttpApiError = class extends Error {
|
|
336
|
+
code;
|
|
337
|
+
status;
|
|
338
|
+
constructor(status, code) {
|
|
339
|
+
super(code || `tidee returned ${status}`);
|
|
340
|
+
this.status = status;
|
|
341
|
+
this.code = code;
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
var call = async (path, init) => {
|
|
345
|
+
const url = new URL(`${API_URL}${path}`);
|
|
346
|
+
for (const [key, value] of Object.entries(init.query ?? {})) {
|
|
347
|
+
if (value !== void 0 && value !== "") {
|
|
348
|
+
url.searchParams.set(key, value);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
let response;
|
|
352
|
+
try {
|
|
353
|
+
response = await fetch(url, {
|
|
354
|
+
method: init.method,
|
|
355
|
+
headers: {
|
|
356
|
+
Authorization: `Bearer ${API_TOKEN}`,
|
|
357
|
+
"Content-Type": "application/json"
|
|
358
|
+
},
|
|
359
|
+
body: init.body === void 0 ? void 0 : JSON.stringify(init.body)
|
|
360
|
+
});
|
|
361
|
+
} catch (error) {
|
|
362
|
+
throw new Error(
|
|
363
|
+
`Could not reach tidee at ${API_URL}. ${error?.message ?? ""}`.trim(),
|
|
364
|
+
{ cause: error }
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
const body = await response.json().catch(() => null);
|
|
368
|
+
if (!response.ok) {
|
|
369
|
+
const code = body?.error?.code;
|
|
370
|
+
throw new HttpApiError(response.status, code);
|
|
371
|
+
}
|
|
372
|
+
return body;
|
|
373
|
+
};
|
|
374
|
+
var api = {
|
|
375
|
+
capture: ({ text, parentId }) => call("/api/v1/capture", {
|
|
376
|
+
method: "POST",
|
|
377
|
+
body: { text, parentId }
|
|
378
|
+
}),
|
|
379
|
+
createItem: ({ title, parentId, targetDate }) => call("/api/v1/items", {
|
|
380
|
+
method: "POST",
|
|
381
|
+
body: { title, parentId, targetDate }
|
|
382
|
+
}),
|
|
383
|
+
listBoxes: () => call("/api/v1/boxes", { method: "GET" }),
|
|
384
|
+
listItems: ({ q, parentId, limit }) => call("/api/v1/items", {
|
|
385
|
+
method: "GET",
|
|
386
|
+
query: {
|
|
387
|
+
...q ? { q } : {},
|
|
388
|
+
...parentId ? { parentId } : {},
|
|
389
|
+
...limit ? { limit: String(limit) } : {}
|
|
390
|
+
}
|
|
391
|
+
}),
|
|
392
|
+
completeItem: ({ id }) => call(`/api/v1/items/${encodeURIComponent(id)}`, {
|
|
393
|
+
method: "PATCH",
|
|
394
|
+
body: { completed: true }
|
|
395
|
+
})
|
|
396
|
+
};
|
|
397
|
+
var server = new McpServer({ name: "tidee", version: "1.0.0" });
|
|
398
|
+
for (const spec of TIDEE_TOOLS) {
|
|
399
|
+
server.registerTool(
|
|
400
|
+
spec.name,
|
|
401
|
+
{
|
|
402
|
+
title: spec.title,
|
|
403
|
+
description: spec.description,
|
|
404
|
+
inputSchema: spec.inputSchema
|
|
405
|
+
},
|
|
406
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
407
|
+
async (args) => {
|
|
408
|
+
try {
|
|
409
|
+
return {
|
|
410
|
+
content: [
|
|
411
|
+
{
|
|
412
|
+
type: "text",
|
|
413
|
+
text: await spec.run(api, args)
|
|
414
|
+
}
|
|
415
|
+
]
|
|
416
|
+
};
|
|
417
|
+
} catch (error) {
|
|
418
|
+
return {
|
|
419
|
+
content: [
|
|
420
|
+
{
|
|
421
|
+
type: "text",
|
|
422
|
+
text: describeToolError(
|
|
423
|
+
error
|
|
424
|
+
)
|
|
425
|
+
}
|
|
426
|
+
],
|
|
427
|
+
isError: true
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
var transport = new StdioServerTransport();
|
|
434
|
+
await server.connect(transport);
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tidee-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for tidee — let an assistant add items to your workspace, read what is there, and mark things done.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"modelcontextprotocol",
|
|
8
|
+
"tidee",
|
|
9
|
+
"todo",
|
|
10
|
+
"ai",
|
|
11
|
+
"assistant"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": "tidee Ltd",
|
|
15
|
+
"homepage": "https://www.tidee.com",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://gitlab.com/joetidee/tidee-life.git",
|
|
19
|
+
"directory": "packages/tidee-life-mcp"
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://www.tidee.com/en/contact"
|
|
23
|
+
},
|
|
24
|
+
"type": "module",
|
|
25
|
+
"main": "./dist/index.js",
|
|
26
|
+
"bin": {
|
|
27
|
+
"tidee-mcp": "dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist/index.js",
|
|
31
|
+
"README.md",
|
|
32
|
+
"LICENSE"
|
|
33
|
+
],
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=20"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "rm -rf dist && tsc --noEmit && esbuild src/index.ts --bundle --platform=node --format=esm --target=node20 --external:@modelcontextprotocol/sdk --external:zod --outfile=dist/index.js",
|
|
42
|
+
"lint": "eslint ./src"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@modelcontextprotocol/sdk": "1.27.1",
|
|
46
|
+
"zod": "^3.25.76"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@eslint/js": "^10.0.1",
|
|
50
|
+
"@types/node": "^22.16.0",
|
|
51
|
+
"esbuild": "^0.28.2",
|
|
52
|
+
"eslint": "^9.21.0",
|
|
53
|
+
"globals": "^17.4.0",
|
|
54
|
+
"tidee-life-core": "workspace:1.0.0",
|
|
55
|
+
"typescript": "^5.8.3",
|
|
56
|
+
"typescript-eslint": "^8.56.1"
|
|
57
|
+
}
|
|
58
|
+
}
|