@aernoud/contactsmcp 0.1.1

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.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import { searchContacts } from "./tools/searchContacts.js";
6
+ import { readContact } from "./tools/readContact.js";
7
+ import { createContact } from "./tools/createContact.js";
8
+ import { updateContact } from "./tools/updateContact.js";
9
+ import { listGroups } from "./tools/listGroups.js";
10
+ import { addToGroup } from "./tools/addToGroup.js";
11
+ const server = new McpServer({
12
+ name: "contactsmcp",
13
+ version: "0.1.0",
14
+ });
15
+ server.tool("search-contacts", "Search contacts in macOS Contacts.app by name, email, or phone number", {
16
+ query: z.string().describe("Search query — matches against name, email, and phone"),
17
+ limit: z.number().optional().default(25).describe("Max results to return"),
18
+ }, async ({ query, limit }) => {
19
+ const contacts = await searchContacts(query, limit);
20
+ return { content: [{ type: "text", text: JSON.stringify(contacts, null, 2) }] };
21
+ });
22
+ server.tool("read-contact", "Get full details of a contact by their Contacts.app ID", {
23
+ contactId: z.string().describe("Contact ID from Contacts.app (from search results)"),
24
+ }, async ({ contactId }) => {
25
+ const contact = await readContact(contactId);
26
+ if (!contact) {
27
+ return { content: [{ type: "text", text: "Contact not found." }] };
28
+ }
29
+ return { content: [{ type: "text", text: JSON.stringify(contact, null, 2) }] };
30
+ });
31
+ server.tool("create-contact", "Create a new contact in macOS Contacts.app", {
32
+ firstName: z.string().describe("First name"),
33
+ lastName: z.string().describe("Last name"),
34
+ email: z.string().optional().describe("Email address"),
35
+ phone: z.string().optional().describe("Phone number"),
36
+ organization: z.string().optional().describe("Organization / company name"),
37
+ }, async ({ firstName, lastName, email, phone, organization }) => {
38
+ const result = await createContact(firstName, lastName, email, phone, organization);
39
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
40
+ });
41
+ server.tool("update-contact", "Update fields on an existing contact in macOS Contacts.app", {
42
+ contactId: z.string().describe("Contact ID from Contacts.app"),
43
+ firstName: z.string().optional().describe("New first name"),
44
+ lastName: z.string().optional().describe("New last name"),
45
+ email: z.string().optional().describe("New primary email address"),
46
+ phone: z.string().optional().describe("New primary phone number"),
47
+ organization: z.string().optional().describe("New organization / company name"),
48
+ }, async ({ contactId, firstName, lastName, email, phone, organization }) => {
49
+ const result = await updateContact(contactId, firstName, lastName, email, phone, organization);
50
+ if (!result) {
51
+ return { content: [{ type: "text", text: "Contact not found." }] };
52
+ }
53
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
54
+ });
55
+ server.tool("list-groups", "List all contact groups in macOS Contacts.app with member counts", {}, async () => {
56
+ const groups = await listGroups();
57
+ return { content: [{ type: "text", text: JSON.stringify(groups, null, 2) }] };
58
+ });
59
+ server.tool("add-to-group", "Add a contact to a group in macOS Contacts.app", {
60
+ contactId: z.string().describe("Contact ID from Contacts.app"),
61
+ groupName: z.string().describe("Name of the group to add the contact to"),
62
+ }, async ({ contactId, groupName }) => {
63
+ const result = await addToGroup(contactId, groupName);
64
+ if (!result) {
65
+ return { content: [{ type: "text", text: "Contact not found." }] };
66
+ }
67
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
68
+ });
69
+ const transport = new StdioServerTransport();
70
+ await server.connect(transport);
@@ -0,0 +1,5 @@
1
+ export declare function addToGroup(contactId: string, groupName: string): Promise<{
2
+ contactId: string;
3
+ groupName: string;
4
+ status: string;
5
+ } | null>;
@@ -0,0 +1,30 @@
1
+ import { runAppleScript, escapeForAppleScript } from "@mailappmcp/shared";
2
+ export async function addToGroup(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
+ add p to 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
+ return null;
25
+ }
26
+ if (trimmed === "GROUP_NOT_FOUND") {
27
+ throw new Error(`Group not found: ${groupName}`);
28
+ }
29
+ return { contactId, groupName, status: "added" };
30
+ }
@@ -0,0 +1,8 @@
1
+ export declare function createContact(firstName: string, lastName: string, email?: string, phone?: string, organization?: string): Promise<{
2
+ id: string;
3
+ firstName: string;
4
+ lastName: string;
5
+ email: string | undefined;
6
+ phone: string | undefined;
7
+ organization: string | undefined;
8
+ }>;
@@ -0,0 +1,25 @@
1
+ import { runAppleScript, escapeForAppleScript } from "@mailappmcp/shared";
2
+ export async function createContact(firstName, lastName, email, phone, organization) {
3
+ const fName = escapeForAppleScript(firstName);
4
+ const lName = escapeForAppleScript(lastName);
5
+ const emailBlock = email
6
+ ? `make new email at end of emails of newPerson with properties {label:"work", value:"${escapeForAppleScript(email)}"}`
7
+ : "";
8
+ const phoneBlock = phone
9
+ ? `make new phone at end of phones of newPerson with properties {label:"mobile", value:"${escapeForAppleScript(phone)}"}`
10
+ : "";
11
+ const orgBlock = organization
12
+ ? `set organization of newPerson to "${escapeForAppleScript(organization)}"`
13
+ : "";
14
+ const script = `
15
+ tell application "Contacts"
16
+ set newPerson to make new person with properties {first name:"${fName}", last name:"${lName}"}
17
+ ${emailBlock}
18
+ ${phoneBlock}
19
+ ${orgBlock}
20
+ save
21
+ return id of newPerson
22
+ end tell`;
23
+ const id = await runAppleScript(script);
24
+ return { id: id.trim(), firstName, lastName, email, phone, organization };
25
+ }
@@ -0,0 +1,3 @@
1
+ export declare function listGroups(): Promise<{
2
+ memberCount: number;
3
+ }[]>;
@@ -0,0 +1,18 @@
1
+ import { runAppleScript, parseRecords, FIELD_SEP, RECORD_SEP } from "@mailappmcp/shared";
2
+ export async function listGroups() {
3
+ const script = `
4
+ tell application "Contacts"
5
+ set output to ""
6
+ repeat with g in every group
7
+ set gCount to 0
8
+ try
9
+ set gCount to count of people of g
10
+ end try
11
+ set output to output & (id of g) & "${FIELD_SEP}" & (name of g) & "${FIELD_SEP}" & gCount & "${RECORD_SEP}"
12
+ end repeat
13
+ return output
14
+ end tell`;
15
+ const raw = await runAppleScript(script);
16
+ const records = parseRecords(raw, ["id", "name", "memberCount"]);
17
+ return records.map((r) => ({ ...r, memberCount: parseInt(r.memberCount, 10) || 0 }));
18
+ }
@@ -0,0 +1,19 @@
1
+ export declare function readContact(contactId: string): Promise<{
2
+ name: string;
3
+ firstName: string;
4
+ lastName: string;
5
+ organization: string;
6
+ emails: {
7
+ label: string;
8
+ value: string;
9
+ }[];
10
+ phones: {
11
+ label: string;
12
+ value: string;
13
+ }[];
14
+ addresses: {
15
+ label: string;
16
+ value: string;
17
+ }[];
18
+ note: string;
19
+ } | null>;
@@ -0,0 +1,80 @@
1
+ import { runAppleScript, escapeForAppleScript, FIELD_SEP } from "@mailappmcp/shared";
2
+ export async function readContact(contactId) {
3
+ const cId = escapeForAppleScript(contactId);
4
+ const script = `
5
+ tell application "Contacts"
6
+ set results to (every person whose id is "${cId}")
7
+ if (count of results) = 0 then
8
+ return "NOT_FOUND"
9
+ end if
10
+ set p to item 1 of results
11
+ set pName to ""
12
+ try
13
+ set pName to name of p
14
+ end try
15
+ set pFirst to ""
16
+ try
17
+ set pFirst to first name of p as string
18
+ if pFirst is "missing value" then set pFirst to ""
19
+ end try
20
+ set pLast to ""
21
+ try
22
+ set pLast to last name of p as string
23
+ if pLast is "missing value" then set pLast to ""
24
+ end try
25
+ set pOrg to ""
26
+ try
27
+ set pOrg to organization of p as string
28
+ if pOrg is "missing value" then set pOrg to ""
29
+ end try
30
+ set emailList to ""
31
+ try
32
+ repeat with e in emails of p
33
+ set emailList to emailList & (label of e) & ":" & (value of e) & ","
34
+ end repeat
35
+ end try
36
+ set phoneList to ""
37
+ try
38
+ repeat with ph in phones of p
39
+ set phoneList to phoneList & (label of ph) & ":" & (value of ph) & ","
40
+ end repeat
41
+ end try
42
+ set addrList to ""
43
+ try
44
+ repeat with a in addresses of p
45
+ set addrList to addrList & (label of a) & ":" & (formatted address of a) & ","
46
+ end repeat
47
+ end try
48
+ set pNote to ""
49
+ try
50
+ set pNote to note of p as string
51
+ if pNote is "missing value" then set pNote to ""
52
+ end try
53
+ return pName & "${FIELD_SEP}" & pFirst & "${FIELD_SEP}" & pLast & "${FIELD_SEP}" & pOrg & "${FIELD_SEP}" & emailList & "${FIELD_SEP}" & phoneList & "${FIELD_SEP}" & addrList & "${FIELD_SEP}" & pNote
54
+ end tell`;
55
+ const raw = await runAppleScript(script);
56
+ if (raw.trim() === "NOT_FOUND") {
57
+ return null;
58
+ }
59
+ const parts = raw.split(FIELD_SEP);
60
+ const parseList = (s) => (s ?? "")
61
+ .trim()
62
+ .split(",")
63
+ .filter(Boolean)
64
+ .map((item) => {
65
+ const colonIdx = item.indexOf(":");
66
+ if (colonIdx === -1)
67
+ return { label: "", value: item };
68
+ return { label: item.slice(0, colonIdx), value: item.slice(colonIdx + 1) };
69
+ });
70
+ return {
71
+ name: (parts[0] ?? "").trim(),
72
+ firstName: (parts[1] ?? "").trim(),
73
+ lastName: (parts[2] ?? "").trim(),
74
+ organization: (parts[3] ?? "").trim(),
75
+ emails: parseList(parts[4] ?? ""),
76
+ phones: parseList(parts[5] ?? ""),
77
+ addresses: parseList(parts[6] ?? ""),
78
+ note: (parts[7] ?? "").trim(),
79
+ };
80
+ }
@@ -0,0 +1 @@
1
+ export declare function searchContacts(query: string, limit?: number): Promise<Record<string, string>[]>;
@@ -0,0 +1,69 @@
1
+ import { runAppleScript, escapeForAppleScript, parseRecords, FIELD_SEP, RECORD_SEP } from "@mailappmcp/shared";
2
+ export async function searchContacts(query, limit = 25) {
3
+ const q = escapeForAppleScript(query);
4
+ const script = `
5
+ tell application "Contacts"
6
+ set matchedPeople to {}
7
+ try
8
+ set nameMatches to (every person whose name contains "${q}")
9
+ repeat with p in nameMatches
10
+ set end of matchedPeople to p
11
+ end repeat
12
+ end try
13
+ if (count of matchedPeople) = 0 then
14
+ try
15
+ set allPeople to every person
16
+ repeat with p in allPeople
17
+ set found to false
18
+ repeat with e in emails of p
19
+ if value of e contains "${q}" then
20
+ set found to true
21
+ exit repeat
22
+ end if
23
+ end repeat
24
+ if not found then
25
+ repeat with ph in phones of p
26
+ if value of ph contains "${q}" then
27
+ set found to true
28
+ exit repeat
29
+ end if
30
+ end repeat
31
+ end if
32
+ if found then
33
+ set end of matchedPeople to p
34
+ end if
35
+ end repeat
36
+ end try
37
+ end if
38
+ set output to ""
39
+ set maxCount to ${limit}
40
+ set i to 0
41
+ repeat with p in matchedPeople
42
+ if i >= maxCount then exit repeat
43
+ set pId to id of p
44
+ set pName to name of p
45
+ set pOrg to ""
46
+ try
47
+ set pOrg to organization of p as string
48
+ end try
49
+ if pOrg is "missing value" then set pOrg to ""
50
+ set pEmail to ""
51
+ try
52
+ if (count of emails of p) > 0 then
53
+ set pEmail to value of first email of p
54
+ end if
55
+ end try
56
+ set pPhone to ""
57
+ try
58
+ if (count of phones of p) > 0 then
59
+ set pPhone to value of first phone of p
60
+ end if
61
+ end try
62
+ set output to output & pId & "${FIELD_SEP}" & pName & "${FIELD_SEP}" & pOrg & "${FIELD_SEP}" & pEmail & "${FIELD_SEP}" & pPhone & "${RECORD_SEP}"
63
+ set i to i + 1
64
+ end repeat
65
+ return output
66
+ end tell`;
67
+ const raw = await runAppleScript(script);
68
+ return parseRecords(raw, ["id", "name", "organization", "email", "phone"]);
69
+ }
@@ -0,0 +1,10 @@
1
+ export declare function updateContact(contactId: string, firstName?: string, lastName?: string, email?: string, phone?: string, organization?: string): Promise<{
2
+ contactId: string;
3
+ updated: {
4
+ firstName: string | undefined;
5
+ lastName: string | undefined;
6
+ email: string | undefined;
7
+ phone: string | undefined;
8
+ organization: string | undefined;
9
+ };
10
+ } | null>;
@@ -0,0 +1,49 @@
1
+ import { runAppleScript, escapeForAppleScript } from "@mailappmcp/shared";
2
+ export async function updateContact(contactId, firstName, lastName, email, phone, organization) {
3
+ const cId = escapeForAppleScript(contactId);
4
+ const firstNameBlock = firstName !== undefined
5
+ ? `set first name of p to "${escapeForAppleScript(firstName)}"`
6
+ : "";
7
+ const lastNameBlock = lastName !== undefined
8
+ ? `set last name of p to "${escapeForAppleScript(lastName)}"`
9
+ : "";
10
+ const orgBlock = organization !== undefined
11
+ ? `set organization of p to "${escapeForAppleScript(organization)}"`
12
+ : "";
13
+ const emailBlock = email !== undefined
14
+ ? `
15
+ if (count of emails of p) > 0 then
16
+ set value of first email of p to "${escapeForAppleScript(email)}"
17
+ else
18
+ make new email at end of emails of p with properties {label:"work", value:"${escapeForAppleScript(email)}"}
19
+ end if`
20
+ : "";
21
+ const phoneBlock = phone !== undefined
22
+ ? `
23
+ if (count of phones of p) > 0 then
24
+ set value of first phone of p to "${escapeForAppleScript(phone)}"
25
+ else
26
+ make new phone at end of phones of p with properties {label:"mobile", value:"${escapeForAppleScript(phone)}"}
27
+ end if`
28
+ : "";
29
+ const script = `
30
+ tell application "Contacts"
31
+ set results to (every person whose id is "${cId}")
32
+ if (count of results) = 0 then
33
+ return "NOT_FOUND"
34
+ end if
35
+ set p to item 1 of results
36
+ ${firstNameBlock}
37
+ ${lastNameBlock}
38
+ ${orgBlock}
39
+ ${emailBlock}
40
+ ${phoneBlock}
41
+ save
42
+ return "ok"
43
+ end tell`;
44
+ const result = await runAppleScript(script);
45
+ if (result.trim() === "NOT_FOUND") {
46
+ return null;
47
+ }
48
+ return { contactId, updated: { firstName, lastName, email, phone, organization } };
49
+ }
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@aernoud/contactsmcp",
3
+ "version": "0.1.1",
4
+ "description": "MCP server for macOS Contacts.app — contact management via AppleScript",
5
+ "mcpName": "io.github.aernouddekker/contactsmcp",
6
+ "type": "module",
7
+ "bin": { "contactsmcp": "./dist/index.js" },
8
+ "files": ["dist"],
9
+ "scripts": { "build": "tsc", "start": "node dist/index.js", "prepublishOnly": "npm run build" },
10
+ "keywords": ["mcp", "mcp-server", "contacts", "address-book", "macos", "applescript", "claude"],
11
+ "author": "Aernoud Dekker",
12
+ "repository": { "type": "git", "url": "https://github.com/aernouddekker/macos-mcp.git", "directory": "packages/contacts" },
13
+ "dependencies": { "@mailappmcp/shared": "*", "@modelcontextprotocol/sdk": "^1.12.0", "zod": "^3.23.0" },
14
+ "devDependencies": { "typescript": "^5.5.0", "@types/node": "^22.0.0" },
15
+ "license": "MIT",
16
+ "engines": { "node": ">=18" },
17
+ "os": ["darwin"]
18
+ }