@aernoud/contactsmcp 0.1.1 → 0.3.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/dist/index.js +71 -0
- package/dist/tools/createGroup.d.ts +4 -0
- package/dist/tools/createGroup.js +12 -0
- package/dist/tools/deleteContact.d.ts +4 -0
- package/dist/tools/deleteContact.js +21 -0
- package/dist/tools/deleteGroup.d.ts +4 -0
- package/dist/tools/deleteGroup.js +21 -0
- package/dist/tools/getMyCard.d.ts +7 -0
- package/dist/tools/getMyCard.js +31 -0
- package/dist/tools/getVcard.d.ts +3 -0
- package/dist/tools/getVcard.js +18 -0
- package/dist/tools/listGroupMembers.d.ts +1 -0
- package/dist/tools/listGroupMembers.js +37 -0
- package/dist/tools/removeFromGroup.d.ts +3 -0
- package/dist/tools/removeFromGroup.js +30 -0
- package/dist/tools/renameGroup.d.ts +5 -0
- package/dist/tools/renameGroup.js +22 -0
- package/dist/tools/searchByModificationDate.d.ts +5 -0
- package/dist/tools/searchByModificationDate.js +37 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -8,6 +8,15 @@ import { createContact } from "./tools/createContact.js";
|
|
|
8
8
|
import { updateContact } from "./tools/updateContact.js";
|
|
9
9
|
import { listGroups } from "./tools/listGroups.js";
|
|
10
10
|
import { addToGroup } from "./tools/addToGroup.js";
|
|
11
|
+
import { removeFromGroup } from "./tools/removeFromGroup.js";
|
|
12
|
+
import { deleteContact } from "./tools/deleteContact.js";
|
|
13
|
+
import { createGroup } from "./tools/createGroup.js";
|
|
14
|
+
import { deleteGroup } from "./tools/deleteGroup.js";
|
|
15
|
+
import { renameGroup } from "./tools/renameGroup.js";
|
|
16
|
+
import { listGroupMembers } from "./tools/listGroupMembers.js";
|
|
17
|
+
import { getMyCard } from "./tools/getMyCard.js";
|
|
18
|
+
import { getVcard } from "./tools/getVcard.js";
|
|
19
|
+
import { searchByModificationDate } from "./tools/searchByModificationDate.js";
|
|
11
20
|
const server = new McpServer({
|
|
12
21
|
name: "contactsmcp",
|
|
13
22
|
version: "0.1.0",
|
|
@@ -66,5 +75,67 @@ server.tool("add-to-group", "Add a contact to a group in macOS Contacts.app", {
|
|
|
66
75
|
}
|
|
67
76
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
68
77
|
});
|
|
78
|
+
server.tool("remove-from-group", "Remove a contact from a group in macOS Contacts.app", {
|
|
79
|
+
contactId: z.string().describe("Contact ID from Contacts.app"),
|
|
80
|
+
groupName: z.string().describe("Name of the group to remove the contact from"),
|
|
81
|
+
}, async ({ contactId, groupName }) => {
|
|
82
|
+
const result = await removeFromGroup(contactId, groupName);
|
|
83
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
84
|
+
});
|
|
85
|
+
server.tool("delete-contact", "Delete a contact from macOS Contacts.app", {
|
|
86
|
+
contactId: z.string().describe("Contact ID from Contacts.app"),
|
|
87
|
+
}, async ({ contactId }) => {
|
|
88
|
+
const result = await deleteContact(contactId);
|
|
89
|
+
if (!result) {
|
|
90
|
+
return { content: [{ type: "text", text: "Contact not found." }] };
|
|
91
|
+
}
|
|
92
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
93
|
+
});
|
|
94
|
+
server.tool("create-group", "Create a new contact group in macOS Contacts.app", {
|
|
95
|
+
name: z.string().describe("Name for the new group"),
|
|
96
|
+
}, async ({ name }) => {
|
|
97
|
+
const result = await createGroup(name);
|
|
98
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
99
|
+
});
|
|
100
|
+
server.tool("delete-group", "Delete a contact group from macOS Contacts.app", {
|
|
101
|
+
groupName: z.string().describe("Name of the group to delete"),
|
|
102
|
+
}, async ({ groupName }) => {
|
|
103
|
+
const result = await deleteGroup(groupName);
|
|
104
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
105
|
+
});
|
|
106
|
+
server.tool("rename-group", "Rename a contact group in macOS Contacts.app", {
|
|
107
|
+
groupName: z.string().describe("Current name of the group"),
|
|
108
|
+
newName: z.string().describe("New name for the group"),
|
|
109
|
+
}, async ({ groupName, newName }) => {
|
|
110
|
+
const result = await renameGroup(groupName, newName);
|
|
111
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
112
|
+
});
|
|
113
|
+
server.tool("list-group-members", "List all contacts in a group in macOS Contacts.app", {
|
|
114
|
+
groupName: z.string().describe("Name of the group"),
|
|
115
|
+
limit: z.number().optional().default(100).describe("Max members to return"),
|
|
116
|
+
}, async ({ groupName, limit }) => {
|
|
117
|
+
const members = await listGroupMembers(groupName, limit);
|
|
118
|
+
return { content: [{ type: "text", text: JSON.stringify(members, null, 2) }] };
|
|
119
|
+
});
|
|
120
|
+
server.tool("get-my-card", "Get the user's own contact card from macOS Contacts.app", {}, async () => {
|
|
121
|
+
const card = await getMyCard();
|
|
122
|
+
return { content: [{ type: "text", text: JSON.stringify(card, null, 2) }] };
|
|
123
|
+
});
|
|
124
|
+
server.tool("get-vcard", "Get the vCard 3.0 text for a contact by their Contacts.app ID", {
|
|
125
|
+
contactId: z.string().describe("Contact ID from Contacts.app (from search results)"),
|
|
126
|
+
}, async ({ contactId }) => {
|
|
127
|
+
const result = await getVcard(contactId);
|
|
128
|
+
if (!result) {
|
|
129
|
+
return { content: [{ type: "text", text: "Contact not found." }] };
|
|
130
|
+
}
|
|
131
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
132
|
+
});
|
|
133
|
+
server.tool("search-by-modification-date", "Find contacts modified after a given date in macOS Contacts.app", {
|
|
134
|
+
since: z.string().describe("Date string, e.g. 'January 1, 2026' or '2026-01-01'"),
|
|
135
|
+
limit: z.number().optional().default(50).describe("Max results to return"),
|
|
136
|
+
}, async ({ since, limit }) => {
|
|
137
|
+
const contacts = await searchByModificationDate(since, limit);
|
|
138
|
+
return { content: [{ type: "text", text: JSON.stringify(contacts, null, 2) }] };
|
|
139
|
+
});
|
|
69
140
|
const transport = new StdioServerTransport();
|
|
70
141
|
await server.connect(transport);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { runAppleScript, escapeForAppleScript } from "@mailappmcp/shared";
|
|
2
|
+
export async function createGroup(name) {
|
|
3
|
+
const gName = escapeForAppleScript(name);
|
|
4
|
+
const script = `
|
|
5
|
+
tell application "Contacts"
|
|
6
|
+
set newGroup to make new group with properties {name:"${gName}"}
|
|
7
|
+
save
|
|
8
|
+
return id of newGroup
|
|
9
|
+
end tell`;
|
|
10
|
+
const id = (await runAppleScript(script)).trim();
|
|
11
|
+
return { id, name };
|
|
12
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { runAppleScript, escapeForAppleScript } from "@mailappmcp/shared";
|
|
2
|
+
export async function deleteContact(contactId) {
|
|
3
|
+
const cId = escapeForAppleScript(contactId);
|
|
4
|
+
const script = `
|
|
5
|
+
tell application "Contacts"
|
|
6
|
+
set personResults to (every person whose id is "${cId}")
|
|
7
|
+
if (count of personResults) = 0 then
|
|
8
|
+
return "CONTACT_NOT_FOUND"
|
|
9
|
+
end if
|
|
10
|
+
set p to item 1 of personResults
|
|
11
|
+
delete p
|
|
12
|
+
save
|
|
13
|
+
return "deleted"
|
|
14
|
+
end tell`;
|
|
15
|
+
const result = await runAppleScript(script);
|
|
16
|
+
const trimmed = result.trim();
|
|
17
|
+
if (trimmed === "CONTACT_NOT_FOUND") {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return { success: true, contactId };
|
|
21
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { runAppleScript, escapeForAppleScript } from "@mailappmcp/shared";
|
|
2
|
+
export async function deleteGroup(groupName) {
|
|
3
|
+
const gName = escapeForAppleScript(groupName);
|
|
4
|
+
const script = `
|
|
5
|
+
tell application "Contacts"
|
|
6
|
+
set groupResults to (every group whose name is "${gName}")
|
|
7
|
+
if (count of groupResults) = 0 then
|
|
8
|
+
return "GROUP_NOT_FOUND"
|
|
9
|
+
end if
|
|
10
|
+
set g to item 1 of groupResults
|
|
11
|
+
delete g
|
|
12
|
+
save
|
|
13
|
+
return "deleted"
|
|
14
|
+
end tell`;
|
|
15
|
+
const result = await runAppleScript(script);
|
|
16
|
+
const trimmed = result.trim();
|
|
17
|
+
if (trimmed === "GROUP_NOT_FOUND") {
|
|
18
|
+
throw new Error(`Group not found: ${groupName}`);
|
|
19
|
+
}
|
|
20
|
+
return { success: true, groupName };
|
|
21
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { runAppleScript, FIELD_SEP } from "@mailappmcp/shared";
|
|
2
|
+
export async function getMyCard() {
|
|
3
|
+
const script = `
|
|
4
|
+
tell application "Contacts"
|
|
5
|
+
set p to my card
|
|
6
|
+
set pId to id of p
|
|
7
|
+
set pName to name of p
|
|
8
|
+
set pEmail to ""
|
|
9
|
+
try
|
|
10
|
+
set pEmail to value of first email of p
|
|
11
|
+
end try
|
|
12
|
+
set pPhone to ""
|
|
13
|
+
try
|
|
14
|
+
set pPhone to value of first phone of p
|
|
15
|
+
end try
|
|
16
|
+
set pOrg to ""
|
|
17
|
+
try
|
|
18
|
+
set pOrg to organization of p
|
|
19
|
+
end try
|
|
20
|
+
return pId & "${FIELD_SEP}" & pName & "${FIELD_SEP}" & pOrg & "${FIELD_SEP}" & pEmail & "${FIELD_SEP}" & pPhone
|
|
21
|
+
end tell`;
|
|
22
|
+
const raw = await runAppleScript(script);
|
|
23
|
+
const parts = raw.split(FIELD_SEP);
|
|
24
|
+
return {
|
|
25
|
+
id: (parts[0] ?? "").trim(),
|
|
26
|
+
name: (parts[1] ?? "").trim(),
|
|
27
|
+
organization: (parts[2] ?? "").trim(),
|
|
28
|
+
email: (parts[3] ?? "").trim(),
|
|
29
|
+
phone: (parts[4] ?? "").trim(),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { runAppleScript, escapeForAppleScript } from "@mailappmcp/shared";
|
|
2
|
+
export async function getVcard(contactId) {
|
|
3
|
+
const cId = escapeForAppleScript(contactId);
|
|
4
|
+
const script = `
|
|
5
|
+
tell application "Contacts"
|
|
6
|
+
try
|
|
7
|
+
set p to first person whose id is "${cId}"
|
|
8
|
+
return vcard of p
|
|
9
|
+
on error
|
|
10
|
+
return "NOT_FOUND"
|
|
11
|
+
end try
|
|
12
|
+
end tell`;
|
|
13
|
+
const raw = await runAppleScript(script);
|
|
14
|
+
if (raw.trim() === "NOT_FOUND") {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
return { vcard: raw.trim() };
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function listGroupMembers(groupName: string, limit?: number): Promise<Record<string, string>[]>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { runAppleScript, escapeForAppleScript, parseRecords, FIELD_SEP, RECORD_SEP } from "@mailappmcp/shared";
|
|
2
|
+
export async function listGroupMembers(groupName, limit = 100) {
|
|
3
|
+
const gName = escapeForAppleScript(groupName);
|
|
4
|
+
const maxCount = Math.floor(limit);
|
|
5
|
+
const script = `
|
|
6
|
+
tell application "Contacts"
|
|
7
|
+
set groupResults to (every group whose name is "${gName}")
|
|
8
|
+
if (count of groupResults) = 0 then
|
|
9
|
+
return "GROUP_NOT_FOUND"
|
|
10
|
+
end if
|
|
11
|
+
set g to item 1 of groupResults
|
|
12
|
+
set output to ""
|
|
13
|
+
set i to 0
|
|
14
|
+
repeat with p in people of g
|
|
15
|
+
if i >= ${maxCount} then exit repeat
|
|
16
|
+
set pId to id of p
|
|
17
|
+
set pName to name of p
|
|
18
|
+
set pEmail to ""
|
|
19
|
+
try
|
|
20
|
+
set pEmail to value of first email of p
|
|
21
|
+
end try
|
|
22
|
+
set pPhone to ""
|
|
23
|
+
try
|
|
24
|
+
set pPhone to value of first phone of p
|
|
25
|
+
end try
|
|
26
|
+
set output to output & pId & "${FIELD_SEP}" & pName & "${FIELD_SEP}" & pEmail & "${FIELD_SEP}" & pPhone & "${RECORD_SEP}"
|
|
27
|
+
set i to i + 1
|
|
28
|
+
end repeat
|
|
29
|
+
return output
|
|
30
|
+
end tell`;
|
|
31
|
+
const result = await runAppleScript(script);
|
|
32
|
+
const trimmed = result.trim();
|
|
33
|
+
if (trimmed === "GROUP_NOT_FOUND") {
|
|
34
|
+
throw new Error(`Group not found: ${groupName}`);
|
|
35
|
+
}
|
|
36
|
+
return parseRecords(trimmed, ["id", "name", "email", "phone"]);
|
|
37
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { runAppleScript, escapeForAppleScript } from "@mailappmcp/shared";
|
|
2
|
+
export async function removeFromGroup(contactId, groupName) {
|
|
3
|
+
const cId = escapeForAppleScript(contactId);
|
|
4
|
+
const gName = escapeForAppleScript(groupName);
|
|
5
|
+
const script = `
|
|
6
|
+
tell application "Contacts"
|
|
7
|
+
set personResults to (every person whose id is "${cId}")
|
|
8
|
+
if (count of personResults) = 0 then
|
|
9
|
+
return "CONTACT_NOT_FOUND"
|
|
10
|
+
end if
|
|
11
|
+
set groupResults to (every group whose name is "${gName}")
|
|
12
|
+
if (count of groupResults) = 0 then
|
|
13
|
+
return "GROUP_NOT_FOUND"
|
|
14
|
+
end if
|
|
15
|
+
set p to item 1 of personResults
|
|
16
|
+
set g to item 1 of groupResults
|
|
17
|
+
remove p from g
|
|
18
|
+
save
|
|
19
|
+
return "done"
|
|
20
|
+
end tell`;
|
|
21
|
+
const result = await runAppleScript(script);
|
|
22
|
+
const trimmed = result.trim();
|
|
23
|
+
if (trimmed === "CONTACT_NOT_FOUND") {
|
|
24
|
+
throw new Error(`Contact not found: ${contactId}`);
|
|
25
|
+
}
|
|
26
|
+
if (trimmed === "GROUP_NOT_FOUND") {
|
|
27
|
+
throw new Error(`Group not found: ${groupName}`);
|
|
28
|
+
}
|
|
29
|
+
return { success: true };
|
|
30
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { runAppleScript, escapeForAppleScript } from "@mailappmcp/shared";
|
|
2
|
+
export async function renameGroup(groupName, newName) {
|
|
3
|
+
const gName = escapeForAppleScript(groupName);
|
|
4
|
+
const gNewName = escapeForAppleScript(newName);
|
|
5
|
+
const script = `
|
|
6
|
+
tell application "Contacts"
|
|
7
|
+
set groupResults to (every group whose name is "${gName}")
|
|
8
|
+
if (count of groupResults) = 0 then
|
|
9
|
+
return "GROUP_NOT_FOUND"
|
|
10
|
+
end if
|
|
11
|
+
set g to item 1 of groupResults
|
|
12
|
+
set name of g to "${gNewName}"
|
|
13
|
+
save
|
|
14
|
+
return "renamed"
|
|
15
|
+
end tell`;
|
|
16
|
+
const result = await runAppleScript(script);
|
|
17
|
+
const trimmed = result.trim();
|
|
18
|
+
if (trimmed === "GROUP_NOT_FOUND") {
|
|
19
|
+
throw new Error(`Group not found: ${groupName}`);
|
|
20
|
+
}
|
|
21
|
+
return { success: true, oldName: groupName, newName };
|
|
22
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { runAppleScript, escapeForAppleScript, FIELD_SEP, RECORD_SEP } from "@mailappmcp/shared";
|
|
2
|
+
export async function searchByModificationDate(since, limit) {
|
|
3
|
+
const escapedDate = escapeForAppleScript(since);
|
|
4
|
+
const script = `
|
|
5
|
+
tell application "Contacts"
|
|
6
|
+
set cutoffDate to date "${escapedDate}"
|
|
7
|
+
set output to ""
|
|
8
|
+
set i to 0
|
|
9
|
+
set maxLimit to ${limit}
|
|
10
|
+
repeat with p in every person
|
|
11
|
+
if i >= maxLimit then exit repeat
|
|
12
|
+
if modification date of p > cutoffDate then
|
|
13
|
+
set pId to id of p
|
|
14
|
+
set pName to name of p
|
|
15
|
+
set modDate to modification date of p as string
|
|
16
|
+
set output to output & pId & "${FIELD_SEP}" & pName & "${FIELD_SEP}" & modDate & "${RECORD_SEP}"
|
|
17
|
+
set i to i + 1
|
|
18
|
+
end if
|
|
19
|
+
end repeat
|
|
20
|
+
return output
|
|
21
|
+
end tell`;
|
|
22
|
+
const raw = await runAppleScript(script);
|
|
23
|
+
if (!raw.trim()) {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
return raw
|
|
27
|
+
.split(RECORD_SEP)
|
|
28
|
+
.filter((r) => r.trim())
|
|
29
|
+
.map((record) => {
|
|
30
|
+
const fields = record.split(FIELD_SEP);
|
|
31
|
+
return {
|
|
32
|
+
id: (fields[0] ?? "").trim(),
|
|
33
|
+
name: (fields[1] ?? "").trim(),
|
|
34
|
+
modificationDate: (fields[2] ?? "").trim(),
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
}
|
package/package.json
CHANGED