Add Contacts MCP tools
This commit is contained in:
@@ -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**.
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user