keepsake-mcp 1.2.0 → 1.4.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 +54 -6
- package/build/index.js +8 -775
- package/build/tools.js +816 -0
- package/package.json +5 -1
package/build/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
import {
|
|
4
|
+
import { registerAllTools } from "./tools.js";
|
|
5
5
|
// ---------------------------------------------------------------------------
|
|
6
6
|
// Configuration
|
|
7
7
|
// ---------------------------------------------------------------------------
|
|
@@ -12,7 +12,10 @@ if (!API_KEY) {
|
|
|
12
12
|
"Generate one at https://app.keepsake.place/account");
|
|
13
13
|
process.exit(1);
|
|
14
14
|
}
|
|
15
|
-
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// HTTP helper bound to the static API key
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
const fetchApi = async (path, method = "GET", body) => {
|
|
16
19
|
const url = `${API_BASE}${path}`;
|
|
17
20
|
const headers = {
|
|
18
21
|
Authorization: `Bearer ${API_KEY}`,
|
|
@@ -30,785 +33,15 @@ async function fetchApi(path, method = "GET", body) {
|
|
|
30
33
|
catch (err) {
|
|
31
34
|
return { error: { code: "NETWORK_ERROR", message: String(err) } };
|
|
32
35
|
}
|
|
33
|
-
}
|
|
34
|
-
/** Format an API result as text content for the MCP response. */
|
|
35
|
-
function toContent(result) {
|
|
36
|
-
if (result.error) {
|
|
37
|
-
const msg = typeof result.error === "string"
|
|
38
|
-
? result.error
|
|
39
|
-
: result.error.message || JSON.stringify(result.error);
|
|
40
|
-
return { content: [{ type: "text", text: `Error: ${msg}` }] };
|
|
41
|
-
}
|
|
42
|
-
return {
|
|
43
|
-
content: [{ type: "text", text: JSON.stringify(result.data ?? result, null, 2) }],
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
/** Build query string from optional params, skipping undefined values. */
|
|
47
|
-
function qs(params) {
|
|
48
|
-
const parts = [];
|
|
49
|
-
for (const [k, v] of Object.entries(params)) {
|
|
50
|
-
if (v !== undefined && v !== null && v !== "") {
|
|
51
|
-
parts.push(`${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return parts.length ? `?${parts.join("&")}` : "";
|
|
55
|
-
}
|
|
36
|
+
};
|
|
56
37
|
// ---------------------------------------------------------------------------
|
|
57
|
-
// MCP Server
|
|
38
|
+
// MCP Server (stdio)
|
|
58
39
|
// ---------------------------------------------------------------------------
|
|
59
40
|
const server = new McpServer({
|
|
60
41
|
name: "keepsake",
|
|
61
42
|
version: "1.0.0",
|
|
62
43
|
});
|
|
63
|
-
|
|
64
|
-
// CONTACTS
|
|
65
|
-
// ===========================================================================
|
|
66
|
-
server.registerTool("list_contacts", {
|
|
67
|
-
description: "List all contacts in the user's Keepsake CRM. Supports pagination, sorting, and optional last_interaction_date enrichment.",
|
|
68
|
-
inputSchema: {
|
|
69
|
-
limit: z.number().int().positive().optional().describe("Max results (default 20)"),
|
|
70
|
-
offset: z.number().int().nonnegative().optional().describe("Pagination offset"),
|
|
71
|
-
sort: z.string().optional().describe("Sort field: last_name, first_name, created_at"),
|
|
72
|
-
order: z.enum(["asc", "desc"]).optional().describe("Sort order"),
|
|
73
|
-
include_last_interaction: z.boolean().optional().describe("Include last_interaction_date for each contact (default: false)"),
|
|
74
|
-
},
|
|
75
|
-
annotations: { title: "List contacts", readOnlyHint: true, openWorldHint: false },
|
|
76
|
-
}, async ({ limit, offset, sort, order, include_last_interaction }) => {
|
|
77
|
-
return toContent(await fetchApi(`/contacts${qs({ limit, offset, sort, order, include_last_interaction })}`));
|
|
78
|
-
});
|
|
79
|
-
server.registerTool("get_contact", {
|
|
80
|
-
description: "Get a single contact by ID, including recent entries (interactions), tags, last_interaction_date, and total_entries count.",
|
|
81
|
-
inputSchema: {
|
|
82
|
-
id: z.string().uuid().describe("Contact UUID"),
|
|
83
|
-
entries_limit: z.number().int().optional().describe("Max entries to return (default 10, -1 for all)"),
|
|
84
|
-
},
|
|
85
|
-
annotations: { title: "Get contact", readOnlyHint: true, openWorldHint: false },
|
|
86
|
-
}, async ({ id, entries_limit }) => {
|
|
87
|
-
return toContent(await fetchApi(`/contacts/${id}${qs({ entries_limit })}`));
|
|
88
|
-
});
|
|
89
|
-
server.registerTool("create_contact", {
|
|
90
|
-
description: "Create a new contact. first_name is required. last_name is optional (useful for contacts where you only know the first name).",
|
|
91
|
-
inputSchema: {
|
|
92
|
-
first_name: z.string().describe("First name"),
|
|
93
|
-
last_name: z.string().optional().describe("Last name (optional)"),
|
|
94
|
-
email: z.string().optional().describe("Email address"),
|
|
95
|
-
phone: z.string().optional().describe("Phone number"),
|
|
96
|
-
company: z.string().optional().describe("Company name"),
|
|
97
|
-
notes: z.string().optional().describe("Notes about the contact"),
|
|
98
|
-
},
|
|
99
|
-
annotations: { title: "Create contact", destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
100
|
-
}, async (params) => {
|
|
101
|
-
return toContent(await fetchApi("/contacts", "POST", params));
|
|
102
|
-
});
|
|
103
|
-
server.registerTool("update_contact", {
|
|
104
|
-
description: "Update an existing contact. Only send the fields you want to change.",
|
|
105
|
-
inputSchema: {
|
|
106
|
-
id: z.string().uuid().describe("Contact UUID"),
|
|
107
|
-
first_name: z.string().optional().describe("First name"),
|
|
108
|
-
last_name: z.string().optional().describe("Last name"),
|
|
109
|
-
email: z.string().optional().describe("Email address"),
|
|
110
|
-
phone: z.string().optional().describe("Phone number"),
|
|
111
|
-
company: z.string().optional().describe("Company name"),
|
|
112
|
-
notes: z.string().optional().describe("Notes about the contact"),
|
|
113
|
-
},
|
|
114
|
-
annotations: { title: "Update contact", destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
115
|
-
}, async ({ id, ...body }) => {
|
|
116
|
-
return toContent(await fetchApi(`/contacts/${id}`, "PATCH", body));
|
|
117
|
-
});
|
|
118
|
-
server.registerTool("delete_contact", {
|
|
119
|
-
description: "Permanently delete a contact and all associated data.",
|
|
120
|
-
inputSchema: {
|
|
121
|
-
id: z.string().uuid().describe("Contact UUID"),
|
|
122
|
-
},
|
|
123
|
-
annotations: { title: "Delete contact", destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
124
|
-
}, async ({ id }) => {
|
|
125
|
-
return toContent(await fetchApi(`/contacts/${id}`, "DELETE"));
|
|
126
|
-
});
|
|
127
|
-
server.registerTool("search_contacts", {
|
|
128
|
-
description: "Search contacts by name, email, company, etc. Search is accent-insensitive.",
|
|
129
|
-
inputSchema: {
|
|
130
|
-
q: z.string().describe("Search query"),
|
|
131
|
-
},
|
|
132
|
-
annotations: { title: "Search contacts", readOnlyHint: true, openWorldHint: false },
|
|
133
|
-
}, async ({ q }) => {
|
|
134
|
-
return toContent(await fetchApi(`/contacts/search${qs({ q })}`));
|
|
135
|
-
});
|
|
136
|
-
// ===========================================================================
|
|
137
|
-
// COMPANIES
|
|
138
|
-
// ===========================================================================
|
|
139
|
-
server.registerTool("list_companies", {
|
|
140
|
-
description: "List all companies/organizations in the user's Keepsake CRM. Supports pagination and sorting.",
|
|
141
|
-
inputSchema: {
|
|
142
|
-
limit: z.number().int().positive().optional().describe("Max results (default 20)"),
|
|
143
|
-
offset: z.number().int().nonnegative().optional().describe("Pagination offset"),
|
|
144
|
-
sort: z.string().optional().describe("Sort field: name, created_at, updated_at"),
|
|
145
|
-
order: z.enum(["asc", "desc"]).optional().describe("Sort order"),
|
|
146
|
-
},
|
|
147
|
-
annotations: { title: "List companies", readOnlyHint: true, openWorldHint: false },
|
|
148
|
-
}, async ({ limit, offset, sort, order }) => {
|
|
149
|
-
return toContent(await fetchApi(`/companies${qs({ limit, offset, sort, order })}`));
|
|
150
|
-
});
|
|
151
|
-
server.registerTool("get_company", {
|
|
152
|
-
description: "Get a single company by ID, including linked contacts (with roles) and tags.",
|
|
153
|
-
inputSchema: {
|
|
154
|
-
id: z.string().uuid().describe("Company UUID"),
|
|
155
|
-
},
|
|
156
|
-
annotations: { title: "Get company", readOnlyHint: true, openWorldHint: false },
|
|
157
|
-
}, async ({ id }) => {
|
|
158
|
-
return toContent(await fetchApi(`/companies/${id}`));
|
|
159
|
-
});
|
|
160
|
-
server.registerTool("create_company", {
|
|
161
|
-
description: "Create a new company/organization. Only 'name' is required.",
|
|
162
|
-
inputSchema: {
|
|
163
|
-
name: z.string().describe("Company name"),
|
|
164
|
-
website: z.string().optional().describe("Website URL"),
|
|
165
|
-
email: z.string().optional().describe("Email address"),
|
|
166
|
-
phone: z.string().optional().describe("Phone number"),
|
|
167
|
-
address: z.string().optional().describe("Address"),
|
|
168
|
-
notes: z.string().optional().describe("Notes about the company"),
|
|
169
|
-
},
|
|
170
|
-
annotations: { title: "Create company", destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
171
|
-
}, async (params) => {
|
|
172
|
-
return toContent(await fetchApi("/companies", "POST", params));
|
|
173
|
-
});
|
|
174
|
-
server.registerTool("update_company", {
|
|
175
|
-
description: "Update an existing company. Only send the fields you want to change.",
|
|
176
|
-
inputSchema: {
|
|
177
|
-
id: z.string().uuid().describe("Company UUID"),
|
|
178
|
-
name: z.string().optional().describe("Company name"),
|
|
179
|
-
website: z.string().optional().describe("Website URL"),
|
|
180
|
-
email: z.string().optional().describe("Email address"),
|
|
181
|
-
phone: z.string().optional().describe("Phone number"),
|
|
182
|
-
address: z.string().optional().describe("Address"),
|
|
183
|
-
notes: z.string().optional().describe("Notes about the company"),
|
|
184
|
-
},
|
|
185
|
-
annotations: { title: "Update company", destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
186
|
-
}, async ({ id, ...body }) => {
|
|
187
|
-
return toContent(await fetchApi(`/companies/${id}`, "PATCH", body));
|
|
188
|
-
});
|
|
189
|
-
server.registerTool("delete_company", {
|
|
190
|
-
description: "Soft-delete a company. Use permanent=true for hard delete.",
|
|
191
|
-
inputSchema: {
|
|
192
|
-
id: z.string().uuid().describe("Company UUID"),
|
|
193
|
-
permanent: z.boolean().optional().describe("Hard delete (default: false, soft delete)"),
|
|
194
|
-
},
|
|
195
|
-
annotations: { title: "Delete company", destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
196
|
-
}, async ({ id, permanent }) => {
|
|
197
|
-
const query = permanent ? "?permanent=true" : "";
|
|
198
|
-
return toContent(await fetchApi(`/companies/${id}${query}`, "DELETE"));
|
|
199
|
-
});
|
|
200
|
-
server.registerTool("search_companies", {
|
|
201
|
-
description: "Search companies by name, email, website, or address. Search is accent-insensitive.",
|
|
202
|
-
inputSchema: {
|
|
203
|
-
q: z.string().describe("Search query"),
|
|
204
|
-
},
|
|
205
|
-
annotations: { title: "Search companies", readOnlyHint: true, openWorldHint: false },
|
|
206
|
-
}, async ({ q }) => {
|
|
207
|
-
return toContent(await fetchApi(`/companies/search${qs({ q })}`));
|
|
208
|
-
});
|
|
209
|
-
// ===========================================================================
|
|
210
|
-
// ENTRIES (Interactions)
|
|
211
|
-
// ===========================================================================
|
|
212
|
-
server.registerTool("list_entries", {
|
|
213
|
-
description: "List interaction entries (calls, emails, meetings, events, etc.). Supports filtering by type, contact, and date range.",
|
|
214
|
-
inputSchema: {
|
|
215
|
-
type: z
|
|
216
|
-
.enum(["call", "email", "meeting", "event", "gift", "letter", "message", "log", "other"])
|
|
217
|
-
.optional()
|
|
218
|
-
.describe("Filter by entry type"),
|
|
219
|
-
contact_id: z.string().uuid().optional().describe("Filter by associated contact ID"),
|
|
220
|
-
from: z.string().optional().describe("Start date (YYYY-MM-DD)"),
|
|
221
|
-
to: z.string().optional().describe("End date (YYYY-MM-DD)"),
|
|
222
|
-
limit: z.number().int().positive().optional().describe("Max results (default 20)"),
|
|
223
|
-
offset: z.number().int().nonnegative().optional().describe("Pagination offset"),
|
|
224
|
-
},
|
|
225
|
-
annotations: { title: "List entries", readOnlyHint: true, openWorldHint: false },
|
|
226
|
-
}, async ({ type, contact_id, from, to, limit, offset }) => {
|
|
227
|
-
return toContent(await fetchApi(`/entries${qs({ type, contact_id, from, to, limit, offset })}`));
|
|
228
|
-
});
|
|
229
|
-
server.registerTool("create_entry", {
|
|
230
|
-
description: "An ENTRY is a dated interaction log tied to contacts. Records something that happened (call, meeting, email…) on a specific date.\n\nCreate a new interaction entry. Content supports #tag# and [[tag]] syntax for automatic tag linking.",
|
|
231
|
-
inputSchema: {
|
|
232
|
-
type: z
|
|
233
|
-
.enum(["call", "email", "meeting", "event", "gift", "letter", "message", "log", "other"])
|
|
234
|
-
.describe("Entry type"),
|
|
235
|
-
date: z.string().describe("Date (YYYY-MM-DD)"),
|
|
236
|
-
content: z.string().optional().describe("Entry content (supports #tag# and [[tag]])"),
|
|
237
|
-
contact_ids: z
|
|
238
|
-
.array(z.string().uuid())
|
|
239
|
-
.optional()
|
|
240
|
-
.describe("Array of contact UUIDs to associate"),
|
|
241
|
-
tag_ids: z
|
|
242
|
-
.array(z.string().uuid())
|
|
243
|
-
.optional()
|
|
244
|
-
.describe("Array of tag UUIDs to link"),
|
|
245
|
-
},
|
|
246
|
-
annotations: { title: "Create entry", destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
247
|
-
}, async (params) => {
|
|
248
|
-
return toContent(await fetchApi("/entries", "POST", params));
|
|
249
|
-
});
|
|
250
|
-
server.registerTool("update_entry", {
|
|
251
|
-
description: "Update an existing entry. Only send fields you want to change.",
|
|
252
|
-
inputSchema: {
|
|
253
|
-
id: z.string().uuid().describe("Entry UUID"),
|
|
254
|
-
type: z
|
|
255
|
-
.enum(["call", "email", "meeting", "event", "gift", "letter", "message", "log", "other"])
|
|
256
|
-
.optional()
|
|
257
|
-
.describe("Entry type"),
|
|
258
|
-
date: z.string().optional().describe("Date (YYYY-MM-DD)"),
|
|
259
|
-
content: z.string().optional().describe("Entry content (supports #tag# and [[tag]])"),
|
|
260
|
-
contact_ids: z
|
|
261
|
-
.array(z.string().uuid())
|
|
262
|
-
.optional()
|
|
263
|
-
.describe("Replace associated contacts"),
|
|
264
|
-
tag_ids: z
|
|
265
|
-
.array(z.string().uuid())
|
|
266
|
-
.optional()
|
|
267
|
-
.describe("Replace associated tags"),
|
|
268
|
-
},
|
|
269
|
-
annotations: { title: "Update entry", destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
270
|
-
}, async ({ id, ...body }) => {
|
|
271
|
-
return toContent(await fetchApi(`/entries/${id}`, "PATCH", body));
|
|
272
|
-
});
|
|
273
|
-
server.registerTool("delete_entry", {
|
|
274
|
-
description: "Delete an interaction entry.",
|
|
275
|
-
inputSchema: {
|
|
276
|
-
id: z.string().uuid().describe("Entry UUID"),
|
|
277
|
-
},
|
|
278
|
-
annotations: { title: "Delete entry", destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
279
|
-
}, async ({ id }) => {
|
|
280
|
-
return toContent(await fetchApi(`/entries/${id}`, "DELETE"));
|
|
281
|
-
});
|
|
282
|
-
// ===========================================================================
|
|
283
|
-
// TASKS
|
|
284
|
-
// ===========================================================================
|
|
285
|
-
server.registerTool("list_tasks", {
|
|
286
|
-
description: "List tasks. Filter by status (pending/completed), date_type, or specific date.",
|
|
287
|
-
inputSchema: {
|
|
288
|
-
status: z.enum(["pending", "completed"]).optional().describe("Filter by status"),
|
|
289
|
-
date_type: z
|
|
290
|
-
.enum(["specific", "asap", "one_day"])
|
|
291
|
-
.optional()
|
|
292
|
-
.describe("Filter by date type: specific (has a due date), asap (do as soon as possible), one_day (someday/no rush)"),
|
|
293
|
-
date: z.string().optional().describe("Filter by specific date (YYYY-MM-DD)"),
|
|
294
|
-
limit: z.number().int().positive().optional().describe("Max results (default 20)"),
|
|
295
|
-
offset: z.number().int().nonnegative().optional().describe("Pagination offset"),
|
|
296
|
-
},
|
|
297
|
-
annotations: { title: "List tasks", readOnlyHint: true, openWorldHint: false },
|
|
298
|
-
}, async ({ status, date_type, date, limit, offset }) => {
|
|
299
|
-
return toContent(await fetchApi(`/tasks${qs({ status, date_type, date, limit, offset })}`));
|
|
300
|
-
});
|
|
301
|
-
server.registerTool("create_task", {
|
|
302
|
-
description: "A TASK is an action item to accomplish. Can have due date, recurrence, priority, linked to contacts and tags.\n\nCreate a new task. Title supports #tag# and [[tag]] for automatic tag linking.",
|
|
303
|
-
inputSchema: {
|
|
304
|
-
title: z.string().describe("Task title (supports #tag# and [[tag]])"),
|
|
305
|
-
description: z.string().optional().describe("Task description"),
|
|
306
|
-
date: z.string().optional().describe("Due date (YYYY-MM-DD)"),
|
|
307
|
-
date_type: z
|
|
308
|
-
.enum(["specific", "asap", "one_day"])
|
|
309
|
-
.optional()
|
|
310
|
-
.describe("Date type: specific (has a due date, default), asap (do as soon as possible, no date needed), one_day (someday/no rush, no date needed)"),
|
|
311
|
-
priority: z.enum(["low", "medium", "high"]).optional().describe("Priority level"),
|
|
312
|
-
recurrence_type: z
|
|
313
|
-
.enum(["daily", "weekly", "monthly", "yearly"])
|
|
314
|
-
.optional()
|
|
315
|
-
.describe("Recurrence pattern"),
|
|
316
|
-
recurrence_interval: z
|
|
317
|
-
.number()
|
|
318
|
-
.int()
|
|
319
|
-
.positive()
|
|
320
|
-
.optional()
|
|
321
|
-
.describe("Recurrence interval (e.g., every N days)"),
|
|
322
|
-
contact_ids: z
|
|
323
|
-
.array(z.string().uuid())
|
|
324
|
-
.optional()
|
|
325
|
-
.describe("Array of contact UUIDs to associate"),
|
|
326
|
-
tag_ids: z
|
|
327
|
-
.array(z.string().uuid())
|
|
328
|
-
.optional()
|
|
329
|
-
.describe("Array of tag UUIDs to link"),
|
|
330
|
-
},
|
|
331
|
-
annotations: { title: "Create task", destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
332
|
-
}, async (params) => {
|
|
333
|
-
return toContent(await fetchApi("/tasks", "POST", params));
|
|
334
|
-
});
|
|
335
|
-
server.registerTool("update_task", {
|
|
336
|
-
description: "Update an existing task. Only send fields you want to change.",
|
|
337
|
-
inputSchema: {
|
|
338
|
-
id: z.string().uuid().describe("Task UUID"),
|
|
339
|
-
title: z.string().optional().describe("Task title"),
|
|
340
|
-
description: z.string().optional().describe("Task description"),
|
|
341
|
-
date: z.string().optional().describe("Due date (YYYY-MM-DD)"),
|
|
342
|
-
date_type: z
|
|
343
|
-
.enum(["specific", "asap", "one_day"])
|
|
344
|
-
.optional()
|
|
345
|
-
.describe("Date type: specific (has a due date), asap (do as soon as possible), one_day (someday/no rush)"),
|
|
346
|
-
priority: z.enum(["low", "medium", "high"]).optional().describe("Priority level"),
|
|
347
|
-
contact_ids: z
|
|
348
|
-
.array(z.string().uuid())
|
|
349
|
-
.optional()
|
|
350
|
-
.describe("Replace associated contacts"),
|
|
351
|
-
tag_ids: z
|
|
352
|
-
.array(z.string().uuid())
|
|
353
|
-
.optional()
|
|
354
|
-
.describe("Replace associated tags"),
|
|
355
|
-
},
|
|
356
|
-
annotations: { title: "Update task", destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
357
|
-
}, async ({ id, ...body }) => {
|
|
358
|
-
return toContent(await fetchApi(`/tasks/${id}`, "PATCH", body));
|
|
359
|
-
});
|
|
360
|
-
server.registerTool("delete_task", {
|
|
361
|
-
description: "Delete a task.",
|
|
362
|
-
inputSchema: {
|
|
363
|
-
id: z.string().uuid().describe("Task UUID"),
|
|
364
|
-
},
|
|
365
|
-
annotations: { title: "Delete task", destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
366
|
-
}, async ({ id }) => {
|
|
367
|
-
return toContent(await fetchApi(`/tasks/${id}`, "DELETE"));
|
|
368
|
-
});
|
|
369
|
-
server.registerTool("complete_task", {
|
|
370
|
-
description: "Mark a task as completed. If the task is recurring, this automatically creates the next occurrence.",
|
|
371
|
-
inputSchema: {
|
|
372
|
-
id: z.string().uuid().describe("Task UUID"),
|
|
373
|
-
},
|
|
374
|
-
annotations: { title: "Complete task", destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
375
|
-
}, async ({ id }) => {
|
|
376
|
-
return toContent(await fetchApi(`/tasks/${id}/complete`, "POST"));
|
|
377
|
-
});
|
|
378
|
-
server.registerTool("uncomplete_task", {
|
|
379
|
-
description: "Mark a completed task as pending again.",
|
|
380
|
-
inputSchema: {
|
|
381
|
-
id: z.string().uuid().describe("Task UUID"),
|
|
382
|
-
},
|
|
383
|
-
annotations: { title: "Uncomplete task", destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
384
|
-
}, async ({ id }) => {
|
|
385
|
-
return toContent(await fetchApi(`/tasks/${id}/uncomplete`, "POST"));
|
|
386
|
-
});
|
|
387
|
-
server.registerTool("snooze_task", {
|
|
388
|
-
description: "Reschedule a task to a new date.",
|
|
389
|
-
inputSchema: {
|
|
390
|
-
id: z.string().uuid().describe("Task UUID"),
|
|
391
|
-
date: z.string().describe("New date (YYYY-MM-DD)"),
|
|
392
|
-
date_type: z
|
|
393
|
-
.enum(["specific", "asap", "one_day"])
|
|
394
|
-
.optional()
|
|
395
|
-
.describe("New date type: specific (has a due date, default), asap (do as soon as possible), one_day (someday/no rush)"),
|
|
396
|
-
},
|
|
397
|
-
annotations: { title: "Snooze task", destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
398
|
-
}, async ({ id, ...body }) => {
|
|
399
|
-
return toContent(await fetchApi(`/tasks/${id}/snooze`, "POST", body));
|
|
400
|
-
});
|
|
401
|
-
// ===========================================================================
|
|
402
|
-
// QUICK NOTES
|
|
403
|
-
// ===========================================================================
|
|
404
|
-
server.registerTool("list_notes", {
|
|
405
|
-
description: "List quick notes. Filter by pinned status or archived status.",
|
|
406
|
-
inputSchema: {
|
|
407
|
-
pinned: z.boolean().optional().describe("Filter pinned notes only"),
|
|
408
|
-
archived: z.boolean().optional().describe("Filter archived notes"),
|
|
409
|
-
limit: z.number().int().positive().optional().describe("Max results (default 20)"),
|
|
410
|
-
offset: z.number().int().nonnegative().optional().describe("Pagination offset"),
|
|
411
|
-
},
|
|
412
|
-
annotations: { title: "List notes", readOnlyHint: true, openWorldHint: false },
|
|
413
|
-
}, async ({ pinned, archived, limit, offset }) => {
|
|
414
|
-
return toContent(await fetchApi(`/notes${qs({ pinned, archived, limit, offset })}`));
|
|
415
|
-
});
|
|
416
|
-
server.registerTool("create_note", {
|
|
417
|
-
description: "A NOTE is a durable text document with no date. Use it for reference material, ideas, checklists, or anything to persist.\n\nCreate a new quick note. Content supports #tag# and [[tag]] for automatic tag linking.",
|
|
418
|
-
inputSchema: {
|
|
419
|
-
content: z.string().describe("Note content (supports #tag# and [[tag]])"),
|
|
420
|
-
is_pinned: z.boolean().optional().describe("Pin the note (default: false)"),
|
|
421
|
-
contact_ids: z
|
|
422
|
-
.array(z.string().uuid())
|
|
423
|
-
.optional()
|
|
424
|
-
.describe("Array of contact UUIDs to associate"),
|
|
425
|
-
},
|
|
426
|
-
annotations: { title: "Create note", destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
427
|
-
}, async (params) => {
|
|
428
|
-
return toContent(await fetchApi("/notes", "POST", params));
|
|
429
|
-
});
|
|
430
|
-
server.registerTool("update_note", {
|
|
431
|
-
description: "Update an existing quick note.",
|
|
432
|
-
inputSchema: {
|
|
433
|
-
id: z.string().uuid().describe("Note UUID"),
|
|
434
|
-
content: z.string().optional().describe("Updated content"),
|
|
435
|
-
contact_ids: z
|
|
436
|
-
.array(z.string().uuid())
|
|
437
|
-
.optional()
|
|
438
|
-
.describe("Replace associated contacts"),
|
|
439
|
-
tag_ids: z
|
|
440
|
-
.array(z.string().uuid())
|
|
441
|
-
.optional()
|
|
442
|
-
.describe("Replace associated tags"),
|
|
443
|
-
},
|
|
444
|
-
annotations: { title: "Update note", destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
445
|
-
}, async ({ id, ...body }) => {
|
|
446
|
-
return toContent(await fetchApi(`/notes/${id}`, "PATCH", body));
|
|
447
|
-
});
|
|
448
|
-
server.registerTool("delete_note", {
|
|
449
|
-
description: "Soft-delete a quick note. Use permanent=true for hard delete.",
|
|
450
|
-
inputSchema: {
|
|
451
|
-
id: z.string().uuid().describe("Note UUID"),
|
|
452
|
-
permanent: z.boolean().optional().describe("Hard delete (default: false, soft delete)"),
|
|
453
|
-
},
|
|
454
|
-
annotations: { title: "Delete note", destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
455
|
-
}, async ({ id, permanent }) => {
|
|
456
|
-
const query = permanent ? "?permanent=true" : "";
|
|
457
|
-
return toContent(await fetchApi(`/notes/${id}${query}`, "DELETE"));
|
|
458
|
-
});
|
|
459
|
-
server.registerTool("pin_note", {
|
|
460
|
-
description: "Pin a quick note so it appears at the top of the list.",
|
|
461
|
-
inputSchema: {
|
|
462
|
-
id: z.string().uuid().describe("Note UUID"),
|
|
463
|
-
},
|
|
464
|
-
annotations: { title: "Pin note", destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
465
|
-
}, async ({ id }) => {
|
|
466
|
-
return toContent(await fetchApi(`/notes/${id}/pin`, "POST"));
|
|
467
|
-
});
|
|
468
|
-
server.registerTool("archive_note", {
|
|
469
|
-
description: "Archive a quick note.",
|
|
470
|
-
inputSchema: {
|
|
471
|
-
id: z.string().uuid().describe("Note UUID"),
|
|
472
|
-
},
|
|
473
|
-
annotations: { title: "Archive note", destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
474
|
-
}, async ({ id }) => {
|
|
475
|
-
return toContent(await fetchApi(`/notes/${id}/archive`, "POST"));
|
|
476
|
-
});
|
|
477
|
-
server.registerTool("restore_note", {
|
|
478
|
-
description: "Restore a deleted or archived quick note.",
|
|
479
|
-
inputSchema: {
|
|
480
|
-
id: z.string().uuid().describe("Note UUID"),
|
|
481
|
-
},
|
|
482
|
-
annotations: { title: "Restore note", destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
483
|
-
}, async ({ id }) => {
|
|
484
|
-
return toContent(await fetchApi(`/notes/${id}/restore`, "POST"));
|
|
485
|
-
});
|
|
486
|
-
// ===========================================================================
|
|
487
|
-
// DAYS (Daily Summaries)
|
|
488
|
-
// ===========================================================================
|
|
489
|
-
server.registerTool("list_days", {
|
|
490
|
-
description: "List daily journal summaries. Filter by date range.",
|
|
491
|
-
inputSchema: {
|
|
492
|
-
from: z.string().optional().describe("Start date (YYYY-MM-DD)"),
|
|
493
|
-
to: z.string().optional().describe("End date (YYYY-MM-DD)"),
|
|
494
|
-
limit: z.number().int().positive().optional().describe("Max results (default 20)"),
|
|
495
|
-
offset: z.number().int().nonnegative().optional().describe("Pagination offset"),
|
|
496
|
-
},
|
|
497
|
-
annotations: { title: "List days", readOnlyHint: true, openWorldHint: false },
|
|
498
|
-
}, async ({ from, to, limit, offset }) => {
|
|
499
|
-
return toContent(await fetchApi(`/days${qs({ from, to, limit, offset })}`));
|
|
500
|
-
});
|
|
501
|
-
server.registerTool("get_day", {
|
|
502
|
-
description: "Get a specific day's journal summary by date.",
|
|
503
|
-
inputSchema: {
|
|
504
|
-
date: z.string().describe("Date (YYYY-MM-DD)"),
|
|
505
|
-
},
|
|
506
|
-
annotations: { title: "Get day", readOnlyHint: true, openWorldHint: false },
|
|
507
|
-
}, async ({ date }) => {
|
|
508
|
-
return toContent(await fetchApi(`/days/${date}`));
|
|
509
|
-
});
|
|
510
|
-
server.registerTool("update_day", {
|
|
511
|
-
description: "Create or update a daily journal summary. If a day entry already exists for this date, it will be updated (upsert).",
|
|
512
|
-
inputSchema: {
|
|
513
|
-
date: z.string().describe("Date (YYYY-MM-DD)"),
|
|
514
|
-
note: z.string().describe("Journal content for the day"),
|
|
515
|
-
},
|
|
516
|
-
annotations: { title: "Update day", destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
517
|
-
}, async (params) => {
|
|
518
|
-
return toContent(await fetchApi("/days", "POST", params));
|
|
519
|
-
});
|
|
520
|
-
// ===========================================================================
|
|
521
|
-
// TAGS
|
|
522
|
-
// ===========================================================================
|
|
523
|
-
server.registerTool("list_tags", {
|
|
524
|
-
description: "List all tags. Tags organize contacts, entries, tasks, notes, and companies.",
|
|
525
|
-
inputSchema: {
|
|
526
|
-
limit: z.number().int().positive().optional().describe("Max results (default 50)"),
|
|
527
|
-
offset: z.number().int().nonnegative().optional().describe("Pagination offset"),
|
|
528
|
-
},
|
|
529
|
-
annotations: { title: "List tags", readOnlyHint: true, openWorldHint: false },
|
|
530
|
-
}, async ({ limit, offset }) => {
|
|
531
|
-
return toContent(await fetchApi(`/tags${qs({ limit, offset })}`));
|
|
532
|
-
});
|
|
533
|
-
server.registerTool("get_tag_items", {
|
|
534
|
-
description: "Get all items linked to a specific tag: contacts, entries, tasks, notes, and companies with counts.",
|
|
535
|
-
inputSchema: {
|
|
536
|
-
id: z.string().uuid().describe("Tag UUID"),
|
|
537
|
-
},
|
|
538
|
-
annotations: { title: "Get tag items", readOnlyHint: true, openWorldHint: false },
|
|
539
|
-
}, async ({ id }) => {
|
|
540
|
-
return toContent(await fetchApi(`/tags/${id}/items`));
|
|
541
|
-
});
|
|
542
|
-
server.registerTool("link_tag", {
|
|
543
|
-
description: "Link an entity (contact, entry, task, note, or company) to a tag.",
|
|
544
|
-
inputSchema: {
|
|
545
|
-
id: z.string().uuid().describe("Tag UUID"),
|
|
546
|
-
entity_type: z
|
|
547
|
-
.enum(["contact", "entry", "task", "note", "company"])
|
|
548
|
-
.describe("Type of entity to link"),
|
|
549
|
-
entity_id: z.string().uuid().describe("UUID of the entity to link"),
|
|
550
|
-
},
|
|
551
|
-
annotations: { title: "Link tag", destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
552
|
-
}, async ({ id, ...body }) => {
|
|
553
|
-
return toContent(await fetchApi(`/tags/${id}/link`, "POST", body));
|
|
554
|
-
});
|
|
555
|
-
server.registerTool("unlink_tag", {
|
|
556
|
-
description: "Remove the link between an entity and a tag.",
|
|
557
|
-
inputSchema: {
|
|
558
|
-
id: z.string().uuid().describe("Tag UUID"),
|
|
559
|
-
entity_type: z
|
|
560
|
-
.enum(["contact", "entry", "task", "note", "company"])
|
|
561
|
-
.describe("Type of entity to unlink"),
|
|
562
|
-
entity_id: z.string().uuid().describe("UUID of the entity to unlink"),
|
|
563
|
-
},
|
|
564
|
-
annotations: { title: "Unlink tag", destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
565
|
-
}, async ({ id, ...body }) => {
|
|
566
|
-
return toContent(await fetchApi(`/tags/${id}/unlink`, "POST", body));
|
|
567
|
-
});
|
|
568
|
-
// ===========================================================================
|
|
569
|
-
// CONTACT LINKS (link/unlink contacts to notes, entries, tasks)
|
|
570
|
-
// ===========================================================================
|
|
571
|
-
server.registerTool("link_note_contact", {
|
|
572
|
-
description: "Link an existing contact to a note.",
|
|
573
|
-
inputSchema: {
|
|
574
|
-
note_id: z.string().uuid().describe("Note UUID"),
|
|
575
|
-
contact_id: z.string().uuid().describe("Contact UUID"),
|
|
576
|
-
},
|
|
577
|
-
annotations: { title: "Link note contact", destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
578
|
-
}, async ({ note_id, contact_id }) => {
|
|
579
|
-
return toContent(await fetchApi(`/notes/${note_id}/contacts/${contact_id}`, "POST"));
|
|
580
|
-
});
|
|
581
|
-
server.registerTool("unlink_note_contact", {
|
|
582
|
-
description: "Remove the link between a contact and a note.",
|
|
583
|
-
inputSchema: {
|
|
584
|
-
note_id: z.string().uuid().describe("Note UUID"),
|
|
585
|
-
contact_id: z.string().uuid().describe("Contact UUID"),
|
|
586
|
-
},
|
|
587
|
-
annotations: { title: "Unlink note contact", destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
588
|
-
}, async ({ note_id, contact_id }) => {
|
|
589
|
-
return toContent(await fetchApi(`/notes/${note_id}/contacts/${contact_id}`, "DELETE"));
|
|
590
|
-
});
|
|
591
|
-
server.registerTool("link_entry_contact", {
|
|
592
|
-
description: "Link an existing contact to an entry.",
|
|
593
|
-
inputSchema: {
|
|
594
|
-
entry_id: z.string().uuid().describe("Entry UUID"),
|
|
595
|
-
contact_id: z.string().uuid().describe("Contact UUID"),
|
|
596
|
-
},
|
|
597
|
-
annotations: { title: "Link entry contact", destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
598
|
-
}, async ({ entry_id, contact_id }) => {
|
|
599
|
-
return toContent(await fetchApi(`/entries/${entry_id}/contacts/${contact_id}`, "POST"));
|
|
600
|
-
});
|
|
601
|
-
server.registerTool("unlink_entry_contact", {
|
|
602
|
-
description: "Remove the link between a contact and an entry.",
|
|
603
|
-
inputSchema: {
|
|
604
|
-
entry_id: z.string().uuid().describe("Entry UUID"),
|
|
605
|
-
contact_id: z.string().uuid().describe("Contact UUID"),
|
|
606
|
-
},
|
|
607
|
-
annotations: { title: "Unlink entry contact", destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
608
|
-
}, async ({ entry_id, contact_id }) => {
|
|
609
|
-
return toContent(await fetchApi(`/entries/${entry_id}/contacts/${contact_id}`, "DELETE"));
|
|
610
|
-
});
|
|
611
|
-
server.registerTool("link_task_contact", {
|
|
612
|
-
description: "Link an existing contact to a task.",
|
|
613
|
-
inputSchema: {
|
|
614
|
-
task_id: z.string().uuid().describe("Task UUID"),
|
|
615
|
-
contact_id: z.string().uuid().describe("Contact UUID"),
|
|
616
|
-
},
|
|
617
|
-
annotations: { title: "Link task contact", destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
618
|
-
}, async ({ task_id, contact_id }) => {
|
|
619
|
-
return toContent(await fetchApi(`/tasks/${task_id}/contacts/${contact_id}`, "POST"));
|
|
620
|
-
});
|
|
621
|
-
server.registerTool("unlink_task_contact", {
|
|
622
|
-
description: "Remove the link between a contact and a task.",
|
|
623
|
-
inputSchema: {
|
|
624
|
-
task_id: z.string().uuid().describe("Task UUID"),
|
|
625
|
-
contact_id: z.string().uuid().describe("Contact UUID"),
|
|
626
|
-
},
|
|
627
|
-
annotations: { title: "Unlink task contact", destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
628
|
-
}, async ({ task_id, contact_id }) => {
|
|
629
|
-
return toContent(await fetchApi(`/tasks/${task_id}/contacts/${contact_id}`, "DELETE"));
|
|
630
|
-
});
|
|
631
|
-
server.registerTool("get_tag", {
|
|
632
|
-
description: "Get a single tag by ID with all its properties (name, description, color, icon, view mode, favorite status).",
|
|
633
|
-
inputSchema: {
|
|
634
|
-
id: z.string().uuid().describe("Tag UUID"),
|
|
635
|
-
},
|
|
636
|
-
annotations: { title: "Get tag", readOnlyHint: true, openWorldHint: false },
|
|
637
|
-
}, async ({ id }) => {
|
|
638
|
-
return toContent(await fetchApi(`/tags/${id}`));
|
|
639
|
-
});
|
|
640
|
-
server.registerTool("create_tag", {
|
|
641
|
-
description: "A TAG is a thematic grouping space. Syntax: #name# or [[name]]. Groups notes, entries, tasks, and contacts.\n\nCreate a new tag. If a tag with the same name already exists, returns the existing tag.",
|
|
642
|
-
inputSchema: {
|
|
643
|
-
name: z.string().describe("Tag name"),
|
|
644
|
-
description: z.string().optional().describe("Tag description"),
|
|
645
|
-
},
|
|
646
|
-
annotations: { title: "Create tag", destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
647
|
-
}, async (body) => {
|
|
648
|
-
return toContent(await fetchApi("/tags", "POST", body));
|
|
649
|
-
});
|
|
650
|
-
server.registerTool("update_tag", {
|
|
651
|
-
description: "Update an existing tag. Only send the fields you want to change. Supports name, description, color, icon, view mode, and favorite status.",
|
|
652
|
-
inputSchema: {
|
|
653
|
-
id: z.string().uuid().describe("Tag UUID"),
|
|
654
|
-
name: z.string().optional().describe("Tag name"),
|
|
655
|
-
description: z.string().optional().describe("Tag description"),
|
|
656
|
-
color: z.string().optional().describe("Tag color (e.g. 'blue', 'red', 'green')"),
|
|
657
|
-
icon: z.string().optional().describe("Tag icon (emoji or icon name)"),
|
|
658
|
-
view_mode: z.string().optional().describe("View mode for the tag page"),
|
|
659
|
-
is_favorite: z.boolean().optional().describe("Whether the tag is a favorite"),
|
|
660
|
-
},
|
|
661
|
-
annotations: { title: "Update tag", destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
662
|
-
}, async ({ id, ...body }) => {
|
|
663
|
-
return toContent(await fetchApi(`/tags/${id}`, "PATCH", body));
|
|
664
|
-
});
|
|
665
|
-
server.registerTool("delete_tag", {
|
|
666
|
-
description: "Permanently delete a tag and all its links to contacts, entries, tasks, notes, and companies.",
|
|
667
|
-
inputSchema: {
|
|
668
|
-
id: z.string().uuid().describe("Tag UUID"),
|
|
669
|
-
},
|
|
670
|
-
annotations: { title: "Delete tag", destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
671
|
-
}, async ({ id }) => {
|
|
672
|
-
return toContent(await fetchApi(`/tags/${id}`, "DELETE"));
|
|
673
|
-
});
|
|
674
|
-
// ===========================================================================
|
|
675
|
-
// TASK HEADERS (Sections)
|
|
676
|
-
// ===========================================================================
|
|
677
|
-
server.registerTool("list_task_headers", {
|
|
678
|
-
description: "List all task headers (section separators used to group tasks on tag pages and day views).",
|
|
679
|
-
inputSchema: {
|
|
680
|
-
limit: z.number().int().positive().optional().describe("Max results (default 50)"),
|
|
681
|
-
offset: z.number().int().nonnegative().optional().describe("Pagination offset"),
|
|
682
|
-
},
|
|
683
|
-
annotations: { title: "List task headers", readOnlyHint: true, openWorldHint: false },
|
|
684
|
-
}, async ({ limit, offset }) => {
|
|
685
|
-
return toContent(await fetchApi(`/task-headers${qs({ limit, offset })}`));
|
|
686
|
-
});
|
|
687
|
-
server.registerTool("get_task_header", {
|
|
688
|
-
description: "Get a single task header by ID.",
|
|
689
|
-
inputSchema: {
|
|
690
|
-
id: z.string().uuid().describe("Task header UUID"),
|
|
691
|
-
},
|
|
692
|
-
annotations: { title: "Get task header", readOnlyHint: true, openWorldHint: false },
|
|
693
|
-
}, async ({ id }) => {
|
|
694
|
-
return toContent(await fetchApi(`/task-headers/${id}`));
|
|
695
|
-
});
|
|
696
|
-
server.registerTool("create_task_header", {
|
|
697
|
-
description: "Create a new task header (section separator). Add it to a tag's items_order to position it.",
|
|
698
|
-
inputSchema: {
|
|
699
|
-
name: z.string().describe("Header name"),
|
|
700
|
-
description: z.string().optional().describe("Optional description below the header"),
|
|
701
|
-
collapsed: z.boolean().optional().describe("Whether the section is collapsed (default false)"),
|
|
702
|
-
},
|
|
703
|
-
annotations: { title: "Create task header", destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
704
|
-
}, async (body) => {
|
|
705
|
-
return toContent(await fetchApi("/task-headers", "POST", body));
|
|
706
|
-
});
|
|
707
|
-
server.registerTool("update_task_header", {
|
|
708
|
-
description: "Update a task header. Only send the fields you want to change.",
|
|
709
|
-
inputSchema: {
|
|
710
|
-
id: z.string().uuid().describe("Task header UUID"),
|
|
711
|
-
name: z.string().optional().describe("Header name"),
|
|
712
|
-
description: z.string().optional().describe("Description below the header"),
|
|
713
|
-
collapsed: z.boolean().optional().describe("Whether the section is collapsed"),
|
|
714
|
-
},
|
|
715
|
-
annotations: { title: "Update task header", destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
716
|
-
}, async ({ id, ...body }) => {
|
|
717
|
-
return toContent(await fetchApi(`/task-headers/${id}`, "PATCH", body));
|
|
718
|
-
});
|
|
719
|
-
server.registerTool("delete_task_header", {
|
|
720
|
-
description: "Permanently delete a task header.",
|
|
721
|
-
inputSchema: {
|
|
722
|
-
id: z.string().uuid().describe("Task header UUID"),
|
|
723
|
-
},
|
|
724
|
-
annotations: { title: "Delete task header", destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
725
|
-
}, async ({ id }) => {
|
|
726
|
-
return toContent(await fetchApi(`/task-headers/${id}`, "DELETE"));
|
|
727
|
-
});
|
|
728
|
-
// ===========================================================================
|
|
729
|
-
// CONTACT TIMELINE
|
|
730
|
-
// ===========================================================================
|
|
731
|
-
server.registerTool("get_contact_timeline", {
|
|
732
|
-
description: "Get a unified, chronological feed of ALL items related to a contact — entries, tasks, and notes — sorted by date (most recent first). Much more efficient than fetching entries, tasks, and notes separately.",
|
|
733
|
-
inputSchema: {
|
|
734
|
-
id: z.string().uuid().describe("Contact UUID"),
|
|
735
|
-
type: z
|
|
736
|
-
.enum(["all", "entries", "tasks", "notes"])
|
|
737
|
-
.optional()
|
|
738
|
-
.describe("Filter by item type (default: all)"),
|
|
739
|
-
from: z.string().optional().describe("Start date filter (YYYY-MM-DD)"),
|
|
740
|
-
to: z.string().optional().describe("End date filter (YYYY-MM-DD)"),
|
|
741
|
-
limit: z.number().int().positive().optional().describe("Max results (default 20)"),
|
|
742
|
-
offset: z.number().int().nonnegative().optional().describe("Pagination offset"),
|
|
743
|
-
},
|
|
744
|
-
annotations: { title: "Get contact timeline", readOnlyHint: true, openWorldHint: false },
|
|
745
|
-
}, async ({ id, type, from, to, limit, offset }) => {
|
|
746
|
-
return toContent(await fetchApi(`/contacts/${id}/timeline${qs({ type, from, to, limit, offset })}`));
|
|
747
|
-
});
|
|
748
|
-
// ===========================================================================
|
|
749
|
-
// SMART TASK VIEWS
|
|
750
|
-
// ===========================================================================
|
|
751
|
-
server.registerTool("get_tasks_today", {
|
|
752
|
-
description: "Get all tasks for today: overdue tasks + tasks due today + ASAP tasks. Each task has a 'category' field ('overdue', 'today', or 'asap'). Includes counts per category.",
|
|
753
|
-
inputSchema: {},
|
|
754
|
-
annotations: { title: "Get today's tasks", readOnlyHint: true, openWorldHint: false },
|
|
755
|
-
}, async () => {
|
|
756
|
-
return toContent(await fetchApi("/tasks/today"));
|
|
757
|
-
});
|
|
758
|
-
server.registerTool("get_tasks_overdue", {
|
|
759
|
-
description: "Get only overdue tasks (pending tasks with a due date before today). Sorted by date ascending (oldest first).",
|
|
760
|
-
inputSchema: {},
|
|
761
|
-
annotations: { title: "Get overdue tasks", readOnlyHint: true, openWorldHint: false },
|
|
762
|
-
}, async () => {
|
|
763
|
-
return toContent(await fetchApi("/tasks/overdue"));
|
|
764
|
-
});
|
|
765
|
-
// ===========================================================================
|
|
766
|
-
// CHANGELOG
|
|
767
|
-
// ===========================================================================
|
|
768
|
-
server.registerTool("get_changelog", {
|
|
769
|
-
description: "Get all items modified since a given timestamp, across all entity types. Perfect for 'heartbeat' checks to see what changed since your last visit. Returns server_time to use as 'since' for the next call.",
|
|
770
|
-
inputSchema: {
|
|
771
|
-
since: z.string().describe("ISO timestamp — only items modified after this time are returned (e.g. 2026-02-11T10:00:00Z)"),
|
|
772
|
-
type: z
|
|
773
|
-
.enum(["all", "contacts", "entries", "tasks", "notes", "days", "companies"])
|
|
774
|
-
.optional()
|
|
775
|
-
.describe("Filter by entity type (default: all)"),
|
|
776
|
-
limit: z.number().int().positive().optional().describe("Max items per entity type (default 50, max 100)"),
|
|
777
|
-
},
|
|
778
|
-
annotations: { title: "Get changelog", readOnlyHint: true, openWorldHint: false },
|
|
779
|
-
}, async ({ since, type, limit }) => {
|
|
780
|
-
return toContent(await fetchApi(`/changelog${qs({ since, type, limit })}`));
|
|
781
|
-
});
|
|
782
|
-
// ===========================================================================
|
|
783
|
-
// SEARCH
|
|
784
|
-
// ===========================================================================
|
|
785
|
-
server.registerTool("search", {
|
|
786
|
-
description: "Search across all Keepsake data — contacts, entries, tasks, notes, and companies. Search is accent-insensitive (e.g., 'berenice' finds 'Bérénice').",
|
|
787
|
-
inputSchema: {
|
|
788
|
-
q: z.string().describe("Search query"),
|
|
789
|
-
type: z
|
|
790
|
-
.enum(["all", "contacts", "entries", "tasks", "notes", "companies"])
|
|
791
|
-
.optional()
|
|
792
|
-
.describe("Limit search to a specific entity type (default: all)"),
|
|
793
|
-
limit: z.number().int().positive().optional().describe("Max results per type (default 10)"),
|
|
794
|
-
},
|
|
795
|
-
annotations: { title: "Search", readOnlyHint: true, openWorldHint: false },
|
|
796
|
-
}, async ({ q, type, limit }) => {
|
|
797
|
-
return toContent(await fetchApi(`/search${qs({ q, type, limit })}`));
|
|
798
|
-
});
|
|
799
|
-
// ---------------------------------------------------------------------------
|
|
800
|
-
// Agent instructions
|
|
801
|
-
// ---------------------------------------------------------------------------
|
|
802
|
-
server.registerTool("get_agent_instructions", {
|
|
803
|
-
description: "Get best practices and instructions for being an effective Keepsake AI agent. Call this at the start of each session to refresh your instructions.",
|
|
804
|
-
inputSchema: {},
|
|
805
|
-
annotations: { title: "Get agent instructions", readOnlyHint: true, openWorldHint: false },
|
|
806
|
-
}, async () => {
|
|
807
|
-
return toContent(await fetchApi("/agent/instructions"));
|
|
808
|
-
});
|
|
809
|
-
// ===========================================================================
|
|
810
|
-
// Start server
|
|
811
|
-
// ===========================================================================
|
|
44
|
+
registerAllTools(server, fetchApi);
|
|
812
45
|
async function main() {
|
|
813
46
|
const transport = new StdioServerTransport();
|
|
814
47
|
await server.connect(transport);
|