keepsake-mcp 1.10.0 → 1.11.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 +7 -4
- package/build/tools.js +45 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -114,17 +114,17 @@ rather than in the chat, which disappears.
|
|
|
114
114
|
|--------|-----------|-------------|
|
|
115
115
|
| `review_note` | `note_id` | Act as the editor of a note: read it, judge form and substance, leave anchored remarks in the margin, never rewrite the text |
|
|
116
116
|
|
|
117
|
-
## Available tools (
|
|
117
|
+
## Available tools (76)
|
|
118
118
|
|
|
119
119
|
### Contacts
|
|
120
120
|
| Tool | Description |
|
|
121
121
|
|------|-------------|
|
|
122
|
-
| `list_contacts` | List
|
|
122
|
+
| `list_contacts` | List contacts with pagination, sorting, field selection and filters (linked company, has_company, updated_since) |
|
|
123
123
|
| `get_contact` | Get a contact with recent interactions, tags, and stats |
|
|
124
|
-
| `create_contact` | Create a new contact |
|
|
124
|
+
| `create_contact` | Create a new contact (`company` links it to a company record, created if missing) |
|
|
125
125
|
| `update_contact` | Update contact fields |
|
|
126
126
|
| `delete_contact` | Permanently delete a contact |
|
|
127
|
-
| `search_contacts` | Accent-insensitive search by name, email, company |
|
|
127
|
+
| `search_contacts` | Accent-insensitive search by name, email, notes, phone and linked company names |
|
|
128
128
|
| `get_contact_timeline` | Unified chronological feed of all items for a contact |
|
|
129
129
|
|
|
130
130
|
### Companies
|
|
@@ -136,6 +136,9 @@ rather than in the chat, which disappears.
|
|
|
136
136
|
| `update_company` | Update company fields |
|
|
137
137
|
| `delete_company` | Soft-delete (or permanent delete) a company |
|
|
138
138
|
| `search_companies` | Accent-insensitive company search |
|
|
139
|
+
| `link_contact_company` | Link a contact to a company (optional role) |
|
|
140
|
+
| `unlink_contact_company` | Remove a contact–company link |
|
|
141
|
+
| `merge_companies` | Merge a duplicate company into another (contacts, entries, tags, details, notes) |
|
|
139
142
|
|
|
140
143
|
### Entries (Interactions)
|
|
141
144
|
| Tool | Description |
|
package/build/tools.js
CHANGED
|
@@ -109,17 +109,24 @@ export function registerAllTools(server, fetchApi) {
|
|
|
109
109
|
// CONTACTS
|
|
110
110
|
// ===========================================================================
|
|
111
111
|
server.registerTool("list_contacts", {
|
|
112
|
-
description: "List
|
|
112
|
+
description: "List contacts in the user's Keepsake CRM. Supports pagination, sorting, field selection (use `fields` to skip long notes when scanning many contacts), filters on linked companies and on last update, and optional last_interaction_date enrichment. Each contact's companies are company records, returned in `companies`.",
|
|
113
113
|
inputSchema: {
|
|
114
|
-
limit: z.number().int().positive().optional().describe("Max results (default 20)"),
|
|
114
|
+
limit: z.number().int().positive().optional().describe("Max results (default 20, max 100)"),
|
|
115
115
|
offset: z.number().int().nonnegative().optional().describe("Pagination offset"),
|
|
116
|
-
sort: z.string().optional().describe("Sort field: last_name, first_name, created_at"),
|
|
116
|
+
sort: z.string().optional().describe("Sort field: last_name, first_name, created_at, updated_at"),
|
|
117
117
|
order: z.enum(["asc", "desc"]).optional().describe("Sort order"),
|
|
118
118
|
include_last_interaction: z.boolean().optional().describe("Include last_interaction_date for each contact (default: false)"),
|
|
119
|
+
fields: z
|
|
120
|
+
.array(z.string())
|
|
121
|
+
.optional()
|
|
122
|
+
.describe("Only return these fields (id is always included). Columns: first_name, last_name, email, phone, job_title, address, birth_day, birth_month, birth_year, notes, created_at, updated_at. Computed: companies, last_interaction_date. Omit for everything."),
|
|
123
|
+
company: z.string().optional().describe("Only contacts linked to this company: a company UUID, or part of its name (case and accents ignored)"),
|
|
124
|
+
has_company: z.boolean().optional().describe("true: only contacts linked to at least one company; false: only contacts without any company"),
|
|
125
|
+
updated_since: z.string().optional().describe("Only contacts updated after this ISO date or timestamp"),
|
|
119
126
|
},
|
|
120
127
|
annotations: { title: "List contacts", readOnlyHint: true, openWorldHint: false },
|
|
121
|
-
}, async ({ limit, offset, sort, order, include_last_interaction }) => {
|
|
122
|
-
return toContent(await fetchApi(`/contacts${qs({ limit, offset, sort, order, include_last_interaction })}`));
|
|
128
|
+
}, async ({ limit, offset, sort, order, include_last_interaction, fields, company, has_company, updated_since }) => {
|
|
129
|
+
return toContent(await fetchApi(`/contacts${qs({ limit, offset, sort, order, include_last_interaction, fields: fields?.join(","), company, has_company, updated_since })}`));
|
|
123
130
|
});
|
|
124
131
|
server.registerTool("get_contact", {
|
|
125
132
|
description: "Get a single contact by ID, including recent entries (interactions), tags, last_interaction_date, and total_entries count.",
|
|
@@ -138,7 +145,7 @@ export function registerAllTools(server, fetchApi) {
|
|
|
138
145
|
last_name: z.string().optional().describe("Last name (optional)"),
|
|
139
146
|
email: z.string().optional().describe("Email address"),
|
|
140
147
|
phone: z.string().optional().describe("Phone number"),
|
|
141
|
-
company: z.string().optional().describe("Company name"),
|
|
148
|
+
company: z.string().optional().describe("Company name. Links the contact to that company record (matched by name ignoring case and accents, created if missing). Adds a link, never removes one. The contact's companies are returned in `companies`."),
|
|
142
149
|
birthday: z.string().optional().describe("Birthday as ISO date string (YYYY-MM-DD), e.g. '1980-02-14'"),
|
|
143
150
|
notes: z.string().optional().describe("Notes about the contact"),
|
|
144
151
|
},
|
|
@@ -154,7 +161,7 @@ export function registerAllTools(server, fetchApi) {
|
|
|
154
161
|
last_name: z.string().optional().describe("Last name"),
|
|
155
162
|
email: z.string().optional().describe("Email address"),
|
|
156
163
|
phone: z.string().optional().describe("Phone number"),
|
|
157
|
-
company: z.string().optional().describe("Company name"),
|
|
164
|
+
company: z.string().optional().describe("Company name. Links the contact to that company record (matched by name ignoring case and accents, created if missing). Adds a link, never removes one. The contact's companies are returned in `companies`."),
|
|
158
165
|
birthday: z.string().nullable().optional().describe("Birthday as ISO date string (YYYY-MM-DD), e.g. '1980-02-14'. Set to null to clear."),
|
|
159
166
|
notes: z.string().optional().describe("Notes about the contact"),
|
|
160
167
|
},
|
|
@@ -244,6 +251,37 @@ export function registerAllTools(server, fetchApi) {
|
|
|
244
251
|
const query = permanent ? "?permanent=true" : "";
|
|
245
252
|
return toContent(await fetchApi(`/companies/${id}${query}`, "DELETE"));
|
|
246
253
|
});
|
|
254
|
+
server.registerTool("link_contact_company", {
|
|
255
|
+
description: "Link a contact to a company record (idempotent: linking twice keeps one link). A contact can belong to several companies. Optionally set the contact's role there. Shortcut when you only know the company's name: create_contact / update_contact with `company`.",
|
|
256
|
+
inputSchema: {
|
|
257
|
+
company_id: z.string().uuid().describe("Company UUID"),
|
|
258
|
+
contact_id: z.string().uuid().describe("Contact UUID"),
|
|
259
|
+
role: z.string().optional().describe("Role or job at this company (e.g. 'Training advisor')"),
|
|
260
|
+
},
|
|
261
|
+
annotations: { title: "Link contact to company", destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
262
|
+
}, async ({ company_id, contact_id, role }) => {
|
|
263
|
+
return toContent(await fetchApi(`/companies/${company_id}/contacts`, "POST", { contact_id, ...(role !== undefined && { role }) }));
|
|
264
|
+
});
|
|
265
|
+
server.registerTool("unlink_contact_company", {
|
|
266
|
+
description: "Remove the link between a contact and a company. Neither the contact nor the company is deleted.",
|
|
267
|
+
inputSchema: {
|
|
268
|
+
company_id: z.string().uuid().describe("Company UUID"),
|
|
269
|
+
contact_id: z.string().uuid().describe("Contact UUID"),
|
|
270
|
+
},
|
|
271
|
+
annotations: { title: "Unlink contact from company", destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
272
|
+
}, async ({ company_id, contact_id }) => {
|
|
273
|
+
return toContent(await fetchApi(`/companies/${company_id}/contacts${qs({ contact_id })}`, "DELETE"));
|
|
274
|
+
});
|
|
275
|
+
server.registerTool("merge_companies", {
|
|
276
|
+
description: "Merge a duplicate company INTO another one: its contacts, entries and tags move to the target (no duplicates), the target's empty details (website, email, phone, address) are filled from it, its notes are appended to the target's, then it is deleted. Use it when two records describe the same organization (e.g. 'CNCP' and 'Cncp'). Confirm with your user which record to keep.",
|
|
277
|
+
inputSchema: {
|
|
278
|
+
source_id: z.string().uuid().describe("UUID of the company to merge away (it will be deleted)"),
|
|
279
|
+
target_id: z.string().uuid().describe("UUID of the company to keep"),
|
|
280
|
+
},
|
|
281
|
+
annotations: { title: "Merge companies", destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
282
|
+
}, async ({ source_id, target_id }) => {
|
|
283
|
+
return toContent(await fetchApi(`/companies/${source_id}/merge`, "POST", { target_id }));
|
|
284
|
+
});
|
|
247
285
|
server.registerTool("search_companies", {
|
|
248
286
|
description: "Search companies by name, email, website, or address. Search is accent-insensitive.",
|
|
249
287
|
inputSchema: {
|