@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 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,4 @@
1
+ export declare function createGroup(name: string): Promise<{
2
+ id: string;
3
+ name: string;
4
+ }>;
@@ -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,4 @@
1
+ export declare function deleteContact(contactId: string): Promise<{
2
+ success: boolean;
3
+ contactId: string;
4
+ } | null>;
@@ -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,4 @@
1
+ export declare function deleteGroup(groupName: string): Promise<{
2
+ success: boolean;
3
+ groupName: string;
4
+ }>;
@@ -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,7 @@
1
+ export declare function getMyCard(): Promise<{
2
+ id: string;
3
+ name: string;
4
+ organization: string;
5
+ email: string;
6
+ phone: string;
7
+ }>;
@@ -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,3 @@
1
+ export declare function getVcard(contactId: string): Promise<{
2
+ vcard: string;
3
+ } | null>;
@@ -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,3 @@
1
+ export declare function removeFromGroup(contactId: string, groupName: string): Promise<{
2
+ success: boolean;
3
+ }>;
@@ -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,5 @@
1
+ export declare function renameGroup(groupName: string, newName: string): Promise<{
2
+ success: boolean;
3
+ oldName: string;
4
+ newName: string;
5
+ }>;
@@ -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,5 @@
1
+ export declare function searchByModificationDate(since: string, limit: number): Promise<{
2
+ id: string;
3
+ name: string;
4
+ modificationDate: string;
5
+ }[]>;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aernoud/contactsmcp",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "MCP server for macOS Contacts.app — contact management via AppleScript",
5
5
  "mcpName": "io.github.aernouddekker/contactsmcp",
6
6
  "type": "module",