Add Contacts MCP tools

This commit is contained in:
Adolfo Reyna
2026-05-26 22:32:00 -04:00
parent b68ca1bebd
commit 5b0c711f45
3 changed files with 156 additions and 1 deletions
+4 -1
View File
@@ -17,6 +17,9 @@ and stays on the local machine.
| `reminders_list_lists` | List reminder lists | | `reminders_list_lists` | List reminder lists |
| `reminders_list` | List reminders | | `reminders_list` | List reminders |
| `reminders_create` | Create a reminder | | `reminders_create` | Create a reminder |
| `contacts_search` | Search contact names and organizations without disclosing contact methods |
| `contacts_read` | Read phone and email details for one selected contact |
| `contacts_create` | Create a contact with optional email and phone details |
There are no destructive tools in the initial server. There are no destructive tools in the initial server.
@@ -101,7 +104,7 @@ For a harness that launches stdio servers, use:
## Permissions and security ## Permissions and security
On first use of a Notes, Calendar, or Reminders tool, macOS may ask for On first use of a Notes, Calendar, Reminders, or Contacts tool, macOS may ask for
Automation access for Node. Permit only the applications you want the server Automation access for Node. Permit only the applications you want the server
to control under **System Settings > Privacy & Security > Automation**. to control under **System Settings > Privacy & Security > Automation**.
+102
View File
@@ -0,0 +1,102 @@
import { runJxa } from "../apple-events.js";
const CONTACTS_SEARCH_SCRIPT = String.raw`
function text(value) {
try { return value ? String(value) : ""; } catch (_) { return ""; }
}
function run(argv) {
const input = JSON.parse(argv[0]);
const app = Application("Contacts");
const query = (input.query || "").toLowerCase();
const people = app.people();
const found = [];
for (let i = 0; i < people.length && found.length < input.limit; i++) {
const person = people[i];
const name = text(person.name());
const organization = text(person.organization());
if (query && (name + "\n" + organization).toLowerCase().indexOf(query) === -1) continue;
found.push({
id: String(person.id()),
name: name,
organization: organization,
modifiedAt: person.modificationDate().toISOString()
});
}
return JSON.stringify(found);
}`;
const CONTACTS_READ_SCRIPT = String.raw`
function text(value) {
try { return value ? String(value) : ""; } catch (_) { return ""; }
}
function items(values) {
return values.map(function (value) {
return { label: text(value.label()), value: text(value.value()) };
});
}
function run(argv) {
const input = JSON.parse(argv[0]);
const app = Application("Contacts");
const people = app.people();
for (let i = 0; i < people.length; i++) {
const person = people[i];
if (String(person.id()) !== input.id) continue;
return JSON.stringify({
id: String(person.id()),
name: text(person.name()),
firstName: text(person.firstName()),
lastName: text(person.lastName()),
organization: text(person.organization()),
jobTitle: text(person.jobTitle()),
emails: items(person.emails()),
phones: items(person.phones()),
modifiedAt: person.modificationDate().toISOString()
});
}
throw new Error("Contact not found");
}`;
const CONTACTS_CREATE_SCRIPT = String.raw`
function run(argv) {
const input = JSON.parse(argv[0]);
const app = Application("Contacts");
const person = app.Person({
firstName: input.firstName || "",
lastName: input.lastName || "",
organization: input.organization || "",
jobTitle: input.jobTitle || "",
note: input.note || ""
});
app.people.push(person);
if (input.email) {
person.emails.push(app.Email({label: input.email.label, value: input.email.value}));
}
if (input.phone) {
person.phones.push(app.Phone({label: input.phone.label, value: input.phone.value}));
}
app.save();
return JSON.stringify({
id: String(person.id()),
name: String(person.name()),
organization: input.organization || ""
});
}`;
export function searchContacts(input) {
return runJxa(CONTACTS_SEARCH_SCRIPT, input);
}
export function readContact(id) {
return runJxa(CONTACTS_READ_SCRIPT, { id });
}
export function createContact(input) {
return runJxa(CONTACTS_CREATE_SCRIPT, input);
}
+50
View File
@@ -6,6 +6,11 @@ import {
listCalendars, listCalendars,
listEvents, listEvents,
} from "./integrations/calendar.js"; } from "./integrations/calendar.js";
import {
createContact,
readContact,
searchContacts,
} from "./integrations/contacts.js";
import { createNote, listNotes, readNote } from "./integrations/notes.js"; import { createNote, listNotes, readNote } from "./integrations/notes.js";
import { import {
createReminder, createReminder,
@@ -140,5 +145,50 @@ export function createMacMiniMcpServer() {
handled(createReminder), handled(createReminder),
); );
server.tool(
"contacts_search",
"Search Contacts by name or organization. Returns metadata only; use contacts_read for contact methods.",
{
query: z.string().optional().describe("Text to search in display name or organization."),
limit: z.number().int().positive().max(100).default(20),
},
handled(searchContacts),
);
server.tool(
"contacts_read",
"Read contact details, including email addresses and phone numbers, after selecting an ID with contacts_search.",
{
id: z.string().min(1).describe("Persistent Contacts person ID."),
},
handled(({ id }) => readContact(id)),
);
server.tool(
"contacts_create",
"Create a new contact with optional email address and phone number.",
{
firstName: z.string().max(200).optional(),
lastName: z.string().max(200).optional(),
organization: z.string().max(300).optional(),
jobTitle: z.string().max(300).optional(),
note: z.string().max(2000).optional(),
email: z.object({
label: z.string().max(100).default("work"),
value: z.string().email(),
}).optional(),
phone: z.object({
label: z.string().max(100).default("mobile"),
value: z.string().min(1).max(100),
}).optional(),
},
handled((input) => {
if (!input.firstName && !input.lastName && !input.organization) {
throw new Error("firstName, lastName, or organization is required.");
}
return createContact(input);
}),
);
return server; return server;
} }